Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6d62e79
Add support for DateTime field for Go, JS, Python and keep the origin…
MOmarMiraj Mar 12, 2025
66bfad1
fmt fix
MOmarMiraj Mar 12, 2025
916ef7b
remove diff for kotlin
MOmarMiraj Mar 12, 2025
05ea27e
remove unused error and add TODOs for the languages that we don't add…
MOmarMiraj Mar 12, 2025
def0131
add todo
MOmarMiraj Mar 12, 2025
4e9246f
change to OffSetDateTime
MOmarMiraj Mar 12, 2025
f9f3ca4
add test cases for datetime and add errors for Datetime for unsupport…
MOmarMiraj Mar 12, 2025
eddbf9a
fix python serialization logic
MOmarMiraj Mar 12, 2025
4ee40f5
Merge branch 'main' into omar/34/add-date-time-support
MOmarMiraj Mar 12, 2025
a7accd9
update typescript update
MOmarMiraj Mar 12, 2025
5b474d6
add key as a match for TS
MOmarMiraj Mar 12, 2025
fc673a8
clean up code a bit
MOmarMiraj Mar 12, 2025
529be9c
add capability to add imports in go as well as address nits in python…
MOmarMiraj Mar 14, 2025
b4d5d3f
fix go imports and typescript spacing
MOmarMiraj Mar 14, 2025
e1a2b92
update imports to be tabs and sort them
MOmarMiraj Mar 14, 2025
ad45688
update expected tests
MOmarMiraj Mar 14, 2025
8371b66
ensure the custom translations are determnistic by sorting them
MOmarMiraj Mar 14, 2025
7a74614
fmt fix
MOmarMiraj Mar 14, 2025
9e39854
update typescript regex to include optional seconds as well as fix go…
MOmarMiraj Mar 14, 2025
82b5ece
add new line after single imports
MOmarMiraj Mar 14, 2025
2ef4d4a
change write import to add import
MOmarMiraj Mar 14, 2025
646f8f2
fix chaining and check if value is string instead of type casting it
MOmarMiraj Mar 14, 2025
2c7717e
ensure custom serialization/deserialzation is determinstic
MOmarMiraj Mar 14, 2025
5056f25
make go imports deterministic and add all numbers for python serializ…
MOmarMiraj Mar 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions core/data/tests/test_byte_translation/input.rs

This file was deleted.

8 changes: 0 additions & 8 deletions core/data/tests/test_byte_translation/output.go

This file was deleted.

24 changes: 0 additions & 24 deletions core/data/tests/test_byte_translation/output.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#[typeshare]
#[serde(rename_all = "camelCase")]
pub struct Foo {
pub time: time::OffsetDateTime,
pub time2: time::OffsetDateTime,
pub time3: time::OffsetDateTime,
pub bytes: Vec<u8>,
pub bytes2: Vec<u8>
}

#[typeshare]
#[serde(rename_all = "camelCase")]
pub struct TwoFoo {
pub time: time::OffsetDateTime,
pub bytes: Vec<u8>,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package proto

import (
"encoding/json"
"time"
)

type Foo struct {
Time time.Time `json:"time"`
Time2 time.Time `json:"time2"`
Time3 time.Time `json:"time3"`
Bytes []byte `json:"bytes"`
Bytes2 []byte `json:"bytes2"`
}
type TwoFoo struct {
Time time.Time `json:"time"`
Bytes []byte `json:"bytes"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

from datetime import datetime
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer
from typing import Annotated


def serialize_binary_data(value: bytes) -> list[int]:
return list(value)

def deserialize_binary_data(value):
if isinstance(value, list):
if all(isinstance(x, int) and 0 <= x <= 255 for x in value):
return bytes(value)
raise ValueError("All elements must be integers in the range 0-255 (u8).")
elif isinstance(value, bytes):
return value
raise TypeError("Content must be a list of integers (0-255) or bytes.")

def serialize_datetime_data(utc_time: datetime) -> str:
return utc_time.strftime("%Y-%m-%dT%H:%M:%SZ")

def parse_rfc3339(date_str: str) -> datetime:
date_formats = [
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.%fZ"
]

for fmt in date_formats:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue

raise ValueError(f"Invalid RFC 3339 date format: {date_str}")

class Foo(BaseModel):
model_config = ConfigDict(populate_by_name=True)

time: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)]
time_2: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)] = Field(alias="time2")
time_3: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)] = Field(alias="time3")
bytes: Annotated[bytes, BeforeValidator(deserialize_binary_data), PlainSerializer(serialize_binary_data)]
bytes_2: Annotated[bytes, BeforeValidator(deserialize_binary_data), PlainSerializer(serialize_binary_data)] = Field(alias="bytes2")

class TwoFoo(BaseModel):
time: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)]
bytes: Annotated[bytes, BeforeValidator(deserialize_binary_data), PlainSerializer(serialize_binary_data)]

Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
export interface Foo {
thisIsBits: Uint8Array;
thisIsRedundant: Uint8Array;
time: Date;
time2: Date;
time3: Date;
bytes: Uint8Array;
bytes2: Uint8Array;
}

export interface TwoFoo {
time: Date;
bytes: Uint8Array;
}

/**
Expand All @@ -10,13 +18,19 @@ export interface Foo {
* These functions allow for flexible encoding and decoding of data, ensuring that complex types are properly handled when converting between TS objects and JSON
*/
export const ReviverFunc = (key: string, value: unknown): unknown => {
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")) {
return new Date(value);
}
if (Array.isArray(value) && value.every(v => Number.isInteger(v) && v >= 0 && v <= 255) && value.length > 0) {
return new Uint8Array(value);
}
return value;
};

export const ReplacerFunc = (key: string, value: unknown): unknown => {
if (value instanceof Date) {
return value.toISOString();
}
if (value instanceof Uint8Array) {
return Array.from(value);
}
Expand Down
22 changes: 20 additions & 2 deletions core/data/tests/test_serde_iso8601/output.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
from __future__ import annotations

from datetime import datetime
from pydantic import BaseModel
from pydantic import BaseModel, BeforeValidator, PlainSerializer
from typing import Annotated


def serialize_datetime_data(utc_time: datetime) -> str:
return utc_time.strftime("%Y-%m-%dT%H:%M:%SZ")

def parse_rfc3339(date_str: str) -> datetime:
date_formats = [
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.%fZ"
]

for fmt in date_formats:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue

raise ValueError(f"Invalid RFC 3339 date format: {date_str}")

class Foo(BaseModel):
time: datetime
time: Annotated[datetime, BeforeValidator(parse_rfc3339), PlainSerializer(serialize_datetime_data)]

43 changes: 35 additions & 8 deletions core/src/language/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use super::CrateTypes;
pub struct Go {
/// Name of the Go package.
pub package: String,
/// HashMap<ModuleName, HashSet<Identifier>
pub imports: HashSet<String>,
/// Conversions from Rust type names to Go type names.
pub type_mappings: HashMap<String, String>,
/// Abbreviations that should be fully uppercased to comply with Go's formatting rules.
Expand Down Expand Up @@ -93,16 +95,17 @@ impl Language for Go {
}
}

let mut body: Vec<u8> = Vec::new();
for thing in &items {
match thing {
RustItem::Enum(e) => self.write_enum(w, e, &types_mapping_to_struct)?,
RustItem::Struct(s) => self.write_struct(w, s)?,
RustItem::Alias(a) => self.write_type_alias(w, a)?,
RustItem::Const(c) => self.write_const(w, c)?,
RustItem::Enum(e) => self.write_enum(&mut body, e, &types_mapping_to_struct)?,
RustItem::Struct(s) => self.write_struct(&mut body, s)?,
RustItem::Alias(a) => self.write_type_alias(&mut body, a)?,
RustItem::Const(c) => self.write_const(&mut body, c)?,
}
}

self.end_file(w)
self.write_all_imports(w)?;
w.write_all(&body)
}

fn type_map(&mut self) -> &HashMap<String, String> {
Expand Down Expand Up @@ -162,6 +165,10 @@ impl Language for Go {
SpecialRustType::Bool => "bool".into(),
SpecialRustType::F32 => "float32".into(),
SpecialRustType::F64 => "float64".into(),
SpecialRustType::DateTime => {
self.add_import("time");
"time.Time".into()
}
})
}

Expand All @@ -176,8 +183,7 @@ impl Language for Go {
)?;
}
writeln!(w, "package {}", self.package)?;
writeln!(w)?;
writeln!(w, "import \"encoding/json\"")?;
self.add_import("encoding/json");
writeln!(w)?;
Ok(())
}
Expand Down Expand Up @@ -536,6 +542,27 @@ func ({short_name} {full_name}) MarshalJSON() ([]byte, error) {{
};
self.acronyms_to_uppercase(&name)
}

fn add_import(&mut self, name: &str) {
self.imports.insert(name.to_string());
}

fn write_all_imports(&self, w: &mut dyn Write) -> std::io::Result<()> {
let mut imports = self.imports.iter().cloned().collect::<Vec<String>>();
imports.sort();
match imports.as_slice() {
[] => return Ok(()),
[import] => writeln!(w, "import \"{import}\"")?,
_ => {
writeln!(w, "import (")?;
for import in imports {
writeln!(w, "\t\"{import}\"")?;
}
writeln!(w, ")")?
}
}
writeln!(w)
}
}

fn write_comment(w: &mut dyn Write, indent: usize, comment: &str) -> std::io::Result<()> {
Expand Down
9 changes: 7 additions & 2 deletions core/src/language/kotlin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ impl Language for Kotlin {
)
}
SpecialRustType::Unit => "Unit".into(),
SpecialRustType::String => "String".into(),
// Char in Kotlin is 16 bits long, so we need to use String
SpecialRustType::Char => "String".into(),
SpecialRustType::String | SpecialRustType::Char => "String".into(),
// https://kotlinlang.org/docs/basic-types.html#integer-types
SpecialRustType::I8 => "Byte".into(),
SpecialRustType::I16 => "Short".into(),
Expand All @@ -90,6 +89,12 @@ impl Language for Kotlin {
SpecialRustType::Bool => "Boolean".into(),
SpecialRustType::F32 => "Float".into(),
SpecialRustType::F64 => "Double".into(),
// TODO: https://github.com/1Password/typeshare/issues/237
SpecialRustType::DateTime => {
return Err(RustTypeFormatError::UnsupportedSpecialType(
special_ty.to_string(),
))
}
})
}

Expand Down
56 changes: 45 additions & 11 deletions core/src/language/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::{collections::HashMap, io::Write};
use super::CrateTypes;

use convert_case::{Case, Casing};
use itertools::Itertools;

// Utility function from the original author of supporting Python
// Since we won't be supporting generics right now, this function is unused and is left here for future reference
Expand Down Expand Up @@ -140,6 +141,7 @@ impl Language for Python {

self.types_for_custom_json_translation
.iter()
.sorted()
.filter_map(|py_type| json_translation_for_type(py_type))
.map(|custom_translation_functions| {
format!(
Expand Down Expand Up @@ -236,6 +238,10 @@ impl Language for Python {
self.format_type(rtype2, generic_types)?
))
}
SpecialRustType::DateTime => {
self.add_import("datetime".to_string(), "datetime".to_string());
Ok("datetime".into())
}
SpecialRustType::Unit => Ok("None".into()),
SpecialRustType::String | SpecialRustType::Char => Ok("str".into()),
SpecialRustType::I8
Expand Down Expand Up @@ -453,6 +459,8 @@ impl Python {
field_type = format!("Optional[{field_type}]");
}
if let Some(custom_translation) = custom_translations {
self.types_for_custom_json_translation
.insert(field_type.clone());
field_type = format!(
"Annotated[{field_type}, BeforeValidator({}), PlainSerializer({})]",
custom_translation.deserialization_name, custom_translation.serialization_name
Expand Down Expand Up @@ -749,25 +757,51 @@ fn handle_model_config(w: &mut dyn Write, python_module: &mut Python, fields: &[
/// acquires custom translation function names if custom serialize/deserialize functions are needed
fn json_translation_for_type(python_type: &str) -> Option<CustomJsonTranslationFunctions> {
// if more custom serialization/deserialization is needed, we can add it here and in the hashmap below
let custom_translations = HashMap::from([(
"bytes",
CustomJsonTranslationFunctions {
serialization_name: "serialize_binary_data".to_owned(),
serialization_content: r#"def serialize_binary_data(value: bytes) -> list[int]:
let custom_translations = HashMap::from([
(
"bytes",
CustomJsonTranslationFunctions {
serialization_name: "serialize_binary_data".to_owned(),
serialization_content: r#"def serialize_binary_data(value: bytes) -> list[int]:
return list(value)"#
.to_owned(),
deserialization_name: "deserialize_binary_data".to_owned(),
deserialization_content: r#"def deserialize_binary_data(value):
.to_owned(),
deserialization_name: "deserialize_binary_data".to_owned(),
deserialization_content: r#"def deserialize_binary_data(value):
if isinstance(value, list):
if all(isinstance(x, int) and 0 <= x <= 255 for x in value):
return bytes(value)
raise ValueError("All elements must be integers in the range 0-255 (u8).")
elif isinstance(value, bytes):
return value
raise TypeError("Content must be a list of integers (0-255) or bytes.")"#
.to_owned(),
},
)]);
.to_owned(),
},
),
(
"datetime",
CustomJsonTranslationFunctions {
serialization_name: "serialize_datetime_data".to_owned(),
serialization_content: r#"def serialize_datetime_data(utc_time: datetime) -> str:
return utc_time.strftime("%Y-%m-%dT%H:%M:%SZ")"#
.to_owned(),
deserialization_name: "parse_rfc3339".to_owned(),
deserialization_content: r#"def parse_rfc3339(date_str: str) -> datetime:
date_formats = [
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.%fZ"
]

for fmt in date_formats:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue

raise ValueError(f"Invalid RFC 3339 date format: {date_str}")"#
.to_owned(),
},
),
]);

custom_translations
.get(python_type)
Expand Down
Loading
Loading