Skip to content

Commit 3a67a6b

Browse files
authored
Merge pull request #236 from 1Password/omar/34/add-date-time-support
Add support for DateTime field for Go, JS, Python
2 parents 3959a3b + 5056f25 commit 3a67a6b

File tree

16 files changed

+294
-97
lines changed

16 files changed

+294
-97
lines changed

core/data/tests/test_byte_translation/input.rs

Lines changed: 0 additions & 6 deletions
This file was deleted.

core/data/tests/test_byte_translation/output.go

Lines changed: 0 additions & 8 deletions
This file was deleted.

core/data/tests/test_byte_translation/output.py

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#[typeshare]
2+
#[serde(rename_all = "camelCase")]
3+
pub struct Foo {
4+
pub time: time::OffsetDateTime,
5+
pub time2: time::OffsetDateTime,
6+
pub time3: time::OffsetDateTime,
7+
pub bytes: Vec<u8>,
8+
pub bytes2: Vec<u8>
9+
}
10+
11+
#[typeshare]
12+
#[serde(rename_all = "camelCase")]
13+
pub struct TwoFoo {
14+
pub time: time::OffsetDateTime,
15+
pub bytes: Vec<u8>,
16+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package proto
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
)
7+
8+
type Foo struct {
9+
Time time.Time `json:"time"`
10+
Time2 time.Time `json:"time2"`
11+
Time3 time.Time `json:"time3"`
12+
Bytes []byte `json:"bytes"`
13+
Bytes2 []byte `json:"bytes2"`
14+
}
15+
type TwoFoo struct {
16+
Time time.Time `json:"time"`
17+
Bytes []byte `json:"bytes"`
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer
5+
from typing import Annotated
6+
7+
8+
def serialize_binary_data(value: bytes) -> list[int]:
9+
return list(value)
10+
11+
def deserialize_binary_data(value):
12+
if isinstance(value, list):
13+
if all(isinstance(x, int) and 0 <= x <= 255 for x in value):
14+
return bytes(value)
15+
raise ValueError("All elements must be integers in the range 0-255 (u8).")
16+
elif isinstance(value, bytes):
17+
return value
18+
raise TypeError("Content must be a list of integers (0-255) or bytes.")
19+
20+
def serialize_datetime_data(utc_time: datetime) -> str:
21+
return utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
22+
23+
def parse_rfc3339(date_str: str) -> datetime:
24+
date_formats = [
25+
"%Y-%m-%dT%H:%M:%SZ",
26+
"%Y-%m-%dT%H:%M:%S.%fZ"
27+
]
28+
29+
for fmt in date_formats:
30+
try:
31+
return datetime.strptime(date_str, fmt)
32+
except ValueError:
33+
continue
34+
35+
raise ValueError(f"Invalid RFC 3339 date format: {date_str}")
36+
37+
class Foo(BaseModel):
38+
model_config = ConfigDict(populate_by_name=True)
39+
40+
time: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)]
41+
time_2: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)] = Field(alias="time2")
42+
time_3: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)] = Field(alias="time3")
43+
bytes: Annotated[bytes, BeforeValidator(deserialize_binary_data), PlainSerializer(serialize_binary_data)]
44+
bytes_2: Annotated[bytes, BeforeValidator(deserialize_binary_data), PlainSerializer(serialize_binary_data)] = Field(alias="bytes2")
45+
46+
class TwoFoo(BaseModel):
47+
time: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)]
48+
bytes: Annotated[bytes, BeforeValidator(deserialize_binary_data), PlainSerializer(serialize_binary_data)]
49+

core/data/tests/test_byte_translation/output.ts renamed to core/data/tests/test_custom_serialize_deserialize_functions/output.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
export interface Foo {
2-
thisIsBits: Uint8Array;
3-
thisIsRedundant: Uint8Array;
2+
time: Date;
3+
time2: Date;
4+
time3: Date;
5+
bytes: Uint8Array;
6+
bytes2: Uint8Array;
7+
}
8+
9+
export interface TwoFoo {
10+
time: Date;
11+
bytes: Uint8Array;
412
}
513

614
/**
@@ -10,13 +18,19 @@ export interface Foo {
1018
* These functions allow for flexible encoding and decoding of data, ensuring that complex types are properly handled when converting between TS objects and JSON
1119
*/
1220
export const ReviverFunc = (key: string, value: unknown): unknown => {
21+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(value) && (key == "time" || key == "time2" || key == "time3")) {
22+
return new Date(value);
23+
}
1324
if (Array.isArray(value) && value.every(v => Number.isInteger(v) && v >= 0 && v <= 255) && value.length > 0) {
1425
return new Uint8Array(value);
1526
}
1627
return value;
1728
};
1829

1930
export const ReplacerFunc = (key: string, value: unknown): unknown => {
31+
if (value instanceof Date) {
32+
return value.toISOString();
33+
}
2034
if (value instanceof Uint8Array) {
2135
return Array.from(value);
2236
}
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
from __future__ import annotations
22

33
from datetime import datetime
4-
from pydantic import BaseModel
4+
from pydantic import BaseModel, BeforeValidator, PlainSerializer
5+
from typing import Annotated
56

67

8+
def serialize_datetime_data(utc_time: datetime) -> str:
9+
return utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
10+
11+
def parse_rfc3339(date_str: str) -> datetime:
12+
date_formats = [
13+
"%Y-%m-%dT%H:%M:%SZ",
14+
"%Y-%m-%dT%H:%M:%S.%fZ"
15+
]
16+
17+
for fmt in date_formats:
18+
try:
19+
return datetime.strptime(date_str, fmt)
20+
except ValueError:
21+
continue
22+
23+
raise ValueError(f"Invalid RFC 3339 date format: {date_str}")
24+
725
class Foo(BaseModel):
8-
time: datetime
26+
time: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)]
927

core/src/language/go.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
rust_types::{RustEnum, RustEnumVariant, RustField, RustStruct, RustTypeAlias},
1010
topsort::topsort,
1111
};
12-
use std::collections::{HashMap, HashSet};
12+
use std::collections::{BTreeSet, HashMap, HashSet};
1313

1414
use super::CrateTypes;
1515

@@ -18,6 +18,8 @@ use super::CrateTypes;
1818
pub struct Go {
1919
/// Name of the Go package.
2020
pub package: String,
21+
/// BTreeSet<PackageName>
22+
pub imports: BTreeSet<String>,
2123
/// Conversions from Rust type names to Go type names.
2224
pub type_mappings: HashMap<String, String>,
2325
/// Abbreviations that should be fully uppercased to comply with Go's formatting rules.
@@ -93,16 +95,17 @@ impl Language for Go {
9395
}
9496
}
9597

98+
let mut body: Vec<u8> = Vec::new();
9699
for thing in &items {
97100
match thing {
98-
RustItem::Enum(e) => self.write_enum(w, e, &types_mapping_to_struct)?,
99-
RustItem::Struct(s) => self.write_struct(w, s)?,
100-
RustItem::Alias(a) => self.write_type_alias(w, a)?,
101-
RustItem::Const(c) => self.write_const(w, c)?,
101+
RustItem::Enum(e) => self.write_enum(&mut body, e, &types_mapping_to_struct)?,
102+
RustItem::Struct(s) => self.write_struct(&mut body, s)?,
103+
RustItem::Alias(a) => self.write_type_alias(&mut body, a)?,
104+
RustItem::Const(c) => self.write_const(&mut body, c)?,
102105
}
103106
}
104-
105-
self.end_file(w)
107+
self.write_all_imports(w)?;
108+
w.write_all(&body)
106109
}
107110

108111
fn type_map(&mut self) -> &HashMap<String, String> {
@@ -162,6 +165,10 @@ impl Language for Go {
162165
SpecialRustType::Bool => "bool".into(),
163166
SpecialRustType::F32 => "float32".into(),
164167
SpecialRustType::F64 => "float64".into(),
168+
SpecialRustType::DateTime => {
169+
self.add_import("time");
170+
"time.Time".into()
171+
}
165172
})
166173
}
167174

@@ -176,8 +183,7 @@ impl Language for Go {
176183
)?;
177184
}
178185
writeln!(w, "package {}", self.package)?;
179-
writeln!(w)?;
180-
writeln!(w, "import \"encoding/json\"")?;
186+
self.add_import("encoding/json");
181187
writeln!(w)?;
182188
Ok(())
183189
}
@@ -536,6 +542,27 @@ func ({short_name} {full_name}) MarshalJSON() ([]byte, error) {{
536542
};
537543
self.acronyms_to_uppercase(&name)
538544
}
545+
546+
fn add_import(&mut self, name: &str) {
547+
self.imports.insert(name.to_string());
548+
}
549+
550+
fn write_all_imports(&self, w: &mut dyn Write) -> std::io::Result<()> {
551+
let mut imports = self.imports.iter().cloned().collect::<Vec<String>>();
552+
imports.sort();
553+
match imports.as_slice() {
554+
[] => return Ok(()),
555+
[import] => writeln!(w, "import \"{import}\"")?,
556+
_ => {
557+
writeln!(w, "import (")?;
558+
for import in imports {
559+
writeln!(w, "\t\"{import}\"")?;
560+
}
561+
writeln!(w, ")")?
562+
}
563+
}
564+
writeln!(w)
565+
}
539566
}
540567

541568
fn write_comment(w: &mut dyn Write, indent: usize, comment: &str) -> std::io::Result<()> {

core/src/language/kotlin.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,8 @@ impl Language for Kotlin {
7474
)
7575
}
7676
SpecialRustType::Unit => "Unit".into(),
77-
SpecialRustType::String => "String".into(),
7877
// Char in Kotlin is 16 bits long, so we need to use String
79-
SpecialRustType::Char => "String".into(),
78+
SpecialRustType::String | SpecialRustType::Char => "String".into(),
8079
// https://kotlinlang.org/docs/basic-types.html#integer-types
8180
SpecialRustType::I8 => "Byte".into(),
8281
SpecialRustType::I16 => "Short".into(),
@@ -90,6 +89,12 @@ impl Language for Kotlin {
9089
SpecialRustType::Bool => "Boolean".into(),
9190
SpecialRustType::F32 => "Float".into(),
9291
SpecialRustType::F64 => "Double".into(),
92+
// TODO: https://github.com/1Password/typeshare/issues/237
93+
SpecialRustType::DateTime => {
94+
return Err(RustTypeFormatError::UnsupportedSpecialType(
95+
special_ty.to_string(),
96+
))
97+
}
9398
})
9499
}
95100

0 commit comments

Comments
 (0)