From 9489c5da5d5233d016cd07e49e0e90fb2a09a3dc Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Fri, 7 Nov 2025 08:27:30 -0300 Subject: [PATCH] Functional TemporalString type for Python serde --- Cargo.lock | 324 ++++++++++++++++++ duper-python/Cargo.toml | 1 + duper-python/python/duper/__init__.py | 2 + duper-python/python/duper/_duper.pyi | 43 ++- duper-python/python/duper/pydantic.py | 34 +- duper-python/src/de.rs | 58 +--- duper-python/src/lib.rs | 6 +- duper-python/src/ser/well_known_type.rs | 186 +++++----- duper-python/src/temporal.rs | 229 +++++++++++++ duper-python/tests/test_pydantic.py | 11 +- duper/src/serde/meta.rs | 245 +++++++------ duper/src/serde/mod.rs | 3 +- ...__serde_meta_tests__serialize_scalars.snap | 2 +- 13 files changed, 858 insertions(+), 286 deletions(-) create mode 100644 duper-python/src/temporal.rs diff --git a/Cargo.lock b/Cargo.lock index ec32914..a63655b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,16 @@ dependencies = [ "serde", ] +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "cc" version = "1.2.41" @@ -257,6 +267,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concolor" version = "0.1.1" @@ -295,6 +315,26 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "duper" version = "0.3.0" @@ -326,6 +366,7 @@ version = "0.3.0" dependencies = [ "duper", "pyo3", + "temporal_rs", ] [[package]] @@ -515,6 +556,93 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -568,6 +696,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "ixdtf" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + [[package]] name = "js-sys" version = "0.3.82" @@ -584,6 +724,18 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.28" @@ -671,6 +823,17 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1093,6 +1256,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stacker" version = "0.1.22" @@ -1134,6 +1303,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tap" version = "1.0.1" @@ -1146,6 +1326,49 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +[[package]] +name = "temporal_rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e8f02fe6a2da8c8d3adff5d3edce159c8f09cec729f19cf718a46d0ba6bca" +dependencies = [ + "core_maths", + "iana-time-zone", + "icu_calendar", + "icu_locale", + "ixdtf", + "num-traits", + "timezone_provider", + "tinystr", + "web-time", + "writeable", +] + +[[package]] +name = "timezone_provider" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83946b4459e5ec8f59ba94fd6d9a6dc3447af7c6d8dbf7d3125c04fa85498ccb" +dependencies = [ + "combine", + "jiff-tzdb", + "tinystr", + "tzif", + "zerotrie", + "zerovec", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -1238,6 +1461,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tzif" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0376dfa52cce372f3b095010fd064fb850a5d8fbfd5be8b0ffa3d64eeab5a5d" +dependencies = [ + "combine", +] + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -1342,6 +1574,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1549,6 +1791,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "wyz" version = "0.5.1" @@ -1564,6 +1812,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -1583,3 +1854,56 @@ dependencies = [ "quote", "syn 2.0.106", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/duper-python/Cargo.toml b/duper-python/Cargo.toml index 7178b23..a9d7343 100644 --- a/duper-python/Cargo.toml +++ b/duper-python/Cargo.toml @@ -23,3 +23,4 @@ pyo3 = { version = "0.27.1", features = [ "extension-module", "generate-import-lib", ] } +temporal_rs = "0.1.1" diff --git a/duper-python/python/duper/__init__.py b/duper-python/python/duper/__init__.py index dd369fd..81cb2bd 100644 --- a/duper-python/python/duper/__init__.py +++ b/duper-python/python/duper/__init__.py @@ -10,6 +10,7 @@ from ._duper import ( load, Duper, DuperType, + TemporalString, ) from .pydantic import BaseModel @@ -21,4 +22,5 @@ __all__ = [ "Duper", "DuperType", "BaseModel", + "TemporalString", ] diff --git a/duper-python/python/duper/_duper.pyi b/duper-python/python/duper/_duper.pyi index a4f092b..2bad1ba 100644 --- a/duper-python/python/duper/_duper.pyi +++ b/duper-python/python/duper/_duper.pyi @@ -10,6 +10,7 @@ __all__ = [ "load", "Duper", "DuperType", + "TemporalString", ] class Duper: @@ -33,9 +34,45 @@ class Duper: def __init__(self, identifier: str | None) -> None: ... @property - def identifier(self) -> str: ... - -DuperType: TypeAlias = "dict[str, DuperType] | list[DuperType] | tuple[DuperType, ...] | str | bytes | int | float | bool | None" + def identifier(self) -> str | None: ... + +class TemporalString: + """A string representing a valid Temporal value. An optional type may be + provided during initialization to enforce strict parsing.""" + + def __init__( + self, + value: str, + type: Literal[ + "Instant", + "ZonedDateTime", + "PlainDate", + "PlainTime", + "PlainDateTime", + "PlainYearMonth", + "PlainMonthDay", + "Duration", + ] + | None, + ) -> None: ... + @property + def type( + self, + ) -> ( + Literal[ + "Instant", + "ZonedDateTime", + "PlainDate", + "PlainTime", + "PlainDateTime", + "PlainYearMonth", + "PlainMonthDay", + "Duration", + ] + | None + ): ... + +DuperType: TypeAlias = "dict[str, DuperType] | list[DuperType] | tuple[DuperType, ...] | str | bytes | TemporalString | int | float | bool | None" """All possible Python return types for Duper values.""" def dumps( diff --git a/duper-python/python/duper/pydantic.py b/duper-python/python/duper/pydantic.py index 1fc4b56..bc4a8a6 100644 --- a/duper-python/python/duper/pydantic.py +++ b/duper-python/python/duper/pydantic.py @@ -46,18 +46,47 @@ class BaseModel(PydanticBaseModel): Foo(bar='duper') """ + def model_dump_duper( + self, + *, + indent: str | int | None = None, + strip_identifiers: bool = False, + minify: bool = False, + ) -> dict[str, object] | str: + """Generates a Duper representation of the model using Duper's `dumps` method. + + If ``indent`` is a positive integer, then Duper array elements and + object members will be pretty-printed with that indent level. The + indent may also be specified as a ``str`` containing spaces and/or + tabs. ``None`` is the most compact representation. + + If ``strip_identifiers`` is ``True``, then this function will strip + all identifiers from the serialized value. + + If ``minify`` is ``True``, then this function will remove any extra + whitespace. This is incompatible with the ``indent`` option.""" + from ._duper import dumps + + return dumps( + self, indent=indent, strip_identifiers=strip_identifiers, minify=minify + ) + @model_serializer(mode="wrap") def serialize_model( self, handler: SerializerFunctionWrapHandler, info: SerializationInfo, *, + indent: str | int | None = None, strip_identifiers: bool = False, + minify: bool = False, ) -> dict[str, object] | str: if info.mode == "duper": from ._duper import dumps - return dumps(self, strip_identifiers=strip_identifiers) + return dumps( + self, indent=indent, strip_identifiers=strip_identifiers, minify=minify + ) return handler(self) # pyright: ignore[reportAny] @classmethod @@ -97,8 +126,11 @@ class BaseModel(PydanticBaseModel): from ._duper import loads loaded = loads(serialized, parse_any=False) + print(loaded) if isinstance(loaded, list): raise ValidationError("cannot validate Duper list") + if isinstance(loaded, tuple): + raise ValidationError("cannot validate Duper tuple") return cls.model_validate( loaded.model_dump(mode="python"), strict=strict, diff --git a/duper-python/src/de.rs b/duper-python/src/de.rs index d52e8a4..d60bf65 100644 --- a/duper-python/src/de.rs +++ b/duper-python/src/de.rs @@ -1,7 +1,7 @@ use duper::{DuperTemporal, visitor::DuperVisitor}; -use pyo3::{prelude::*, types::*}; +use pyo3::{IntoPyObjectExt, prelude::*, types::*}; -use crate::Duper; +use crate::{Duper, temporal::TemporalString}; #[derive(Clone)] pub(crate) struct Visitor<'py> { @@ -127,59 +127,7 @@ impl<'py> DuperVisitor for Visitor<'py> { temporal: &DuperTemporal<'a>, ) -> Self::Value { Ok(VisitorValue { - value: match temporal { - DuperTemporal::Instant(inner) => self - .py - .import("duper.temporal")? - .getattr("Instant")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::ZonedDateTime(inner) => self - .py - .import("duper.temporal")? - .getattr("ZonedDateTime")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::PlainDate(inner) => self - .py - .import("duper.temporal")? - .getattr("PlainDate")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::PlainTime(inner) => self - .py - .import("duper.temporal")? - .getattr("PlainTime")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::PlainDateTime(inner) => self - .py - .import("duper.temporal")? - .getattr("PlainDateTime")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::PlainYearMonth(inner) => self - .py - .import("duper.temporal")? - .getattr("PlainYearMonth")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::PlainMonthDay(inner) => self - .py - .import("duper.temporal")? - .getattr("PlainMonthDay")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::Duration(inner) => self - .py - .import("duper.temporal")? - .getattr("Duration")? - .getattr("from_string")? - .call1((PyString::new(self.py, inner.as_ref()),))?, - DuperTemporal::Unspecified(inner) => { - PyString::new(self.py, inner.as_ref()).into_any() - } - }, + value: TemporalString::from_temporal(temporal)?.into_bound_py_any(self.py)?, duper: identifier .map(|identifier| Duper::from_identifier(identifier)?.into_pyobject(self.py)) .transpose()?, diff --git a/duper-python/src/lib.rs b/duper-python/src/lib.rs index 51d4bef..bd08d41 100644 --- a/duper-python/src/lib.rs +++ b/duper-python/src/lib.rs @@ -2,11 +2,13 @@ use pyo3::{exceptions::PyValueError, prelude::*}; mod de; mod ser; +mod temporal; #[pyclass(frozen)] pub(crate) struct DuperType; -#[pyclass(frozen)] +#[pyclass(frozen, module = "duper")] +#[derive(Debug, Clone)] pub(crate) struct Duper { pub(crate) identifier: Option>, } @@ -61,7 +63,7 @@ mod duper_py { }; #[pymodule_export] - use crate::{Duper, DuperType}; + use crate::{Duper, DuperType, temporal::TemporalString}; use crate::{de::Visitor, ser::serialize_pyany}; #[pyfunction] diff --git a/duper-python/src/ser/well_known_type.rs b/duper-python/src/ser/well_known_type.rs index d65a51b..0d4a110 100644 --- a/duper-python/src/ser/well_known_type.rs +++ b/duper-python/src/ser/well_known_type.rs @@ -9,6 +9,7 @@ use pyo3::{exceptions::PyValueError, prelude::*, types::*}; use crate::{ Duper, ser::{serialize_pyany, serialize_pyclass_identifier}, + temporal::TemporalString, }; #[derive(Debug)] @@ -90,15 +91,16 @@ pub(crate) enum WellKnownType<'py> { Ulid(Bound<'py, PyAny>), // re Pattern(Bound<'py, PyAny>), - // temporal - TemporalInstant(Bound<'py, PyAny>), - TemporalZonedDateTime(Bound<'py, PyAny>), - TemporalPlainDate(Bound<'py, PyAny>), - TemporalPlainTime(Bound<'py, PyAny>), - TemporalPlainDateTime(Bound<'py, PyAny>), - TemporalPlainYearMonth(Bound<'py, PyAny>), - TemporalPlainMonthDay(Bound<'py, PyAny>), - TemporalDuration(Bound<'py, PyAny>), + // temporal string + TemporalStringInstant(duper::DuperTemporal<'static>), + TemporalStringZonedDateTime(duper::DuperTemporal<'static>), + TemporalStringPlainDate(duper::DuperTemporal<'static>), + TemporalStringPlainTime(duper::DuperTemporal<'static>), + TemporalStringPlainDateTime(duper::DuperTemporal<'static>), + TemporalStringPlainYearMonth(duper::DuperTemporal<'static>), + TemporalStringPlainMonthDay(duper::DuperTemporal<'static>), + TemporalStringDuration(duper::DuperTemporal<'static>), + TemporalString(duper::DuperTemporal<'static>), // uuid Uuid(Bound<'py, PyAny>), } @@ -307,31 +309,6 @@ impl<'py> WellKnownType<'py> { } // re ("re", "Pattern") => return Ok(Some(WellKnownType::Pattern(value.clone()))), - // temporal - ("duper.temporal", "Instant") => { - return Ok(Some(WellKnownType::TemporalInstant(value.clone()))); - } - ("duper.temporal", "ZonedDateTime") => { - return Ok(Some(WellKnownType::TemporalZonedDateTime(value.clone()))); - } - ("duper.temporal", "PlainDate") => { - return Ok(Some(WellKnownType::TemporalPlainDate(value.clone()))); - } - ("duper.temporal", "PlainTime") => { - return Ok(Some(WellKnownType::TemporalPlainTime(value.clone()))); - } - ("duper.temporal", "PlainDateTime") => { - return Ok(Some(WellKnownType::TemporalPlainDateTime(value.clone()))); - } - ("duper.temporal", "PlainYearMonth") => { - return Ok(Some(WellKnownType::TemporalPlainYearMonth(value.clone()))); - } - ("duper.temporal", "PlainMonthDay") => { - return Ok(Some(WellKnownType::TemporalPlainMonthDay(value.clone()))); - } - ("duper.temporal", "Duration") => { - return Ok(Some(WellKnownType::TemporalDuration(value.clone()))); - } // uuid ("uuid", "UUID") => return Ok(Some(WellKnownType::Uuid(value.clone()))), _ => (), @@ -345,6 +322,53 @@ impl<'py> WellKnownType<'py> { { return Ok(Some(WellKnownType::Dataclass(value.clone()))); } + // temporal string + if let Ok(value) = value.cast::() { + let temporal = &value.get().temporal; + match temporal { + DuperTemporal::Instant(_) => { + return Ok(Some(WellKnownType::TemporalStringInstant(temporal.clone()))); + } + DuperTemporal::ZonedDateTime(_) => { + return Ok(Some(WellKnownType::TemporalStringZonedDateTime( + temporal.clone(), + ))); + } + DuperTemporal::PlainDate(_) => { + return Ok(Some(WellKnownType::TemporalStringPlainDate( + temporal.clone(), + ))); + } + DuperTemporal::PlainTime(_) => { + return Ok(Some(WellKnownType::TemporalStringPlainTime( + temporal.clone(), + ))); + } + DuperTemporal::PlainDateTime(_) => { + return Ok(Some(WellKnownType::TemporalStringPlainDateTime( + temporal.clone(), + ))); + } + DuperTemporal::PlainYearMonth(_) => { + return Ok(Some(WellKnownType::TemporalStringPlainYearMonth( + temporal.clone(), + ))); + } + DuperTemporal::PlainMonthDay(_) => { + return Ok(Some(WellKnownType::TemporalStringPlainMonthDay( + temporal.clone(), + ))); + } + DuperTemporal::Duration(_) => { + return Ok(Some(WellKnownType::TemporalStringDuration( + temporal.clone(), + ))); + } + DuperTemporal::Unspecified(_) => { + return Ok(Some(WellKnownType::TemporalString(temporal.clone()))); + } + } + } Ok(None) } @@ -844,102 +868,50 @@ impl<'py> WellKnownType<'py> { value.getattr("pattern")?.extract()?, ))), }), - // temporal - WellKnownType::TemporalInstant(value) => Ok(DuperValue { + // temporal string + WellKnownType::TemporalStringInstant(value) => Ok(DuperValue { identifier: Some(DuperIdentifier::try_from("Instant").expect("valid identifier")), - inner: DuperInner::Temporal( - DuperTemporal::try_instant_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse Instant: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalZonedDateTime(value) => Ok(DuperValue { + WellKnownType::TemporalStringZonedDateTime(value) => Ok(DuperValue { identifier: Some( DuperIdentifier::try_from("ZonedDateTime").expect("valid identifier"), ), - inner: DuperInner::Temporal( - DuperTemporal::try_zoned_date_time_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse ZonedDateTime: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalPlainDate(value) => Ok(DuperValue { + WellKnownType::TemporalStringPlainDate(value) => Ok(DuperValue { identifier: Some(DuperIdentifier::try_from("PlainDate").expect("valid identifier")), - inner: DuperInner::Temporal( - DuperTemporal::try_plain_date_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse PlainDate: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalPlainTime(value) => Ok(DuperValue { + WellKnownType::TemporalStringPlainTime(value) => Ok(DuperValue { identifier: Some(DuperIdentifier::try_from("PlainTime").expect("valid identifier")), - inner: DuperInner::Temporal( - DuperTemporal::try_plain_time_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse PlainTime: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalPlainDateTime(value) => Ok(DuperValue { + WellKnownType::TemporalStringPlainDateTime(value) => Ok(DuperValue { identifier: Some( DuperIdentifier::try_from("PlainDateTime").expect("valid identifier"), ), - inner: DuperInner::Temporal( - DuperTemporal::try_plain_date_time_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse PlainDateTime: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalPlainYearMonth(value) => Ok(DuperValue { + WellKnownType::TemporalStringPlainYearMonth(value) => Ok(DuperValue { identifier: Some( DuperIdentifier::try_from("PlainYearMonth").expect("valid identifier"), ), - inner: DuperInner::Temporal( - DuperTemporal::try_plain_year_month_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse PlainYearMonth: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalPlainMonthDay(value) => Ok(DuperValue { + WellKnownType::TemporalStringPlainMonthDay(value) => Ok(DuperValue { identifier: Some( DuperIdentifier::try_from("PlainMonthDay").expect("valid identifier"), ), - inner: DuperInner::Temporal( - DuperTemporal::try_plain_month_day_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse PlainMonthDay: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), }), - WellKnownType::TemporalDuration(value) => Ok(DuperValue { + WellKnownType::TemporalStringDuration(value) => Ok(DuperValue { identifier: Some(DuperIdentifier::try_from("Duration").expect("valid identifier")), - inner: DuperInner::Temporal( - DuperTemporal::try_duration_from(Cow::Owned( - value.getattr("to_string")?.call0()?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse Duration: {err}")) - })?, - ), + inner: DuperInner::Temporal(value), + }), + WellKnownType::TemporalString(value) => Ok(DuperValue { + identifier: None, + inner: DuperInner::Temporal(value), }), // uuid WellKnownType::Uuid(value) => Ok(DuperValue { diff --git a/duper-python/src/temporal.rs b/duper-python/src/temporal.rs new file mode 100644 index 0000000..b18463c --- /dev/null +++ b/duper-python/src/temporal.rs @@ -0,0 +1,229 @@ +use std::borrow::Cow; + +use pyo3::{ + IntoPyObjectExt, + exceptions::PyValueError, + prelude::*, + types::{PyCFunction, PyDict, PyTuple, PyType}, +}; + +use crate::ser::serialize_pyany; + +#[pyclass(frozen, module = "duper", eq)] +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct TemporalString { + pub(crate) temporal: duper::DuperTemporal<'static>, +} + +impl TemporalString { + pub(crate) fn from_temporal<'a>(temporal: &duper::DuperTemporal<'a>) -> PyResult { + Ok(Self { + temporal: temporal.static_clone(), + }) + } +} + +#[pymethods] +impl TemporalString { + #[new] + fn new(value: String, r#type: Option) -> PyResult { + Ok(TemporalString { + temporal: match r#type.as_ref().map(|typ| typ.as_str()) { + Some("Instant") => duper::DuperTemporal::try_instant_from(Cow::Owned(value)) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse Instant Temporal value: {err}" + )) + })?, + Some("ZonedDateTime") => duper::DuperTemporal::try_zoned_date_time_from( + Cow::Owned(value), + ) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse ZonedDateTime Temporal value: {err}" + )) + })?, + Some("PlainDate") => duper::DuperTemporal::try_plain_date_from(Cow::Owned(value)) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse PlainDate Temporal value: {err}" + )) + })?, + Some("PlainTime") => duper::DuperTemporal::try_plain_time_from(Cow::Owned(value)) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse PlainTime Temporal value: {err}" + )) + })?, + Some("PlainDateTime") => duper::DuperTemporal::try_plain_date_time_from( + Cow::Owned(value), + ) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse PlainDateTime Temporal value: {err}" + )) + })?, + Some("PlainYearMonth") => duper::DuperTemporal::try_plain_year_month_from( + Cow::Owned(value), + ) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse PlainYearMonth Temporal value: {err}" + )) + })?, + Some("PlainMonthDay") => duper::DuperTemporal::try_plain_month_day_from( + Cow::Owned(value), + ) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse PlainMonthDay Temporal value: {err}" + )) + })?, + Some("Duration") => duper::DuperTemporal::try_duration_from(Cow::Owned(value)) + .map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse Duration Temporal value: {err}" + )) + })?, + Some(typ) => { + return Err(PyValueError::new_err(format!( + "Unknown TemporalString type {typ}" + ))); + } + None => duper::DuperTemporal::try_unspecified_from(Cow::Owned(value)).map_err( + |err| { + PyValueError::new_err(format!( + "Failed to parse Unspecified Temporal value: {err}" + )) + }, + )?, + }, + }) + } + + #[getter] + fn r#type(&self) -> Option<&str> { + match self.temporal { + duper::DuperTemporal::Instant(_) => Some("Instant"), + duper::DuperTemporal::ZonedDateTime(_) => Some("ZonedDateTime"), + duper::DuperTemporal::PlainDate(_) => Some("PlainDate"), + duper::DuperTemporal::PlainTime(_) => Some("PlainTime"), + duper::DuperTemporal::PlainDateTime(_) => Some("PlainDateTime"), + duper::DuperTemporal::PlainYearMonth(_) => Some("PlainYearMonth"), + duper::DuperTemporal::PlainMonthDay(_) => Some("PlainMonthDay"), + duper::DuperTemporal::Duration(_) => Some("Duration"), + duper::DuperTemporal::Unspecified(_) => None, + } + } + + fn __repr__(&self) -> String { + let typ = match self.r#type() { + Some(typ) => typ, + None => "None", + }; + let value = self.temporal.as_ref(); + format!("TemporalString(type={typ} value='{value}')") + } + + fn __str__(&self) -> String { + self.temporal.as_ref().to_string() + } + + #[staticmethod] + fn _validate<'py>(value: &Bound<'py, PyAny>) -> PyResult { + if let Ok(temporal) = value.cast::() { + Ok(temporal.get().clone()) + } else if let Ok(value) = value.extract() { + Ok(Self { + temporal: duper::DuperTemporal::try_unspecified_from(Cow::Owned(value)).map_err( + |err| { + PyValueError::new_err(format!( + "Failed to parse Unspecified Temporal value: {err}" + )) + }, + )?, + }) + } else if let Ok(duper::DuperValue { + identifier: _, + inner: duper::DuperInner::Temporal(temporal), + }) = serialize_pyany(value.clone()) + { + Ok(Self { + temporal: temporal.static_clone(), + }) + } else { + let typ = value.get_type(); + Err(PyValueError::new_err(format!( + "Cannot convert {typ} to TemporalString" + ))) + } + } + + #[classmethod] + fn __get_pydantic_core_schema__<'py>( + cls: &Bound<'py, PyType>, + _source: &Bound<'py, PyType>, + _handler: &Bound<'py, PyAny>, + ) -> PyResult> { + let py = cls.py(); + let core_schema = py.import("pydantic_core")?.getattr("core_schema")?; + + let serialization_kwargs = PyDict::new(py); + serialization_kwargs.set_item("info_arg", false)?; + + let serialize = + |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<_> { + let py = args.py(); + let value = args.extract::<(TemporalString,)>()?.0; + match value.temporal { + duper::DuperTemporal::Instant(inner) + | duper::DuperTemporal::PlainDateTime(inner) => { + let datetime = py.import("datetime")?.getattr("datetime")?; + Ok(datetime + .getattr("fromisoformat")? + .call1((inner.as_ref(),))? + .unbind()) + } + duper::DuperTemporal::PlainDate(inner) => { + let date = py.import("datetime")?.getattr("date")?; + Ok(date + .getattr("fromisoformat")? + .call1((inner.as_ref(),))? + .unbind()) + } + duper::DuperTemporal::PlainTime(inner) => { + let time = py.import("datetime")?.getattr("time")?; + Ok(time + .getattr("fromisoformat")? + .call1((inner.as_ref(),))? + .unbind()) + } + duper::DuperTemporal::Duration(inner) => { + let timedelta = py.import("datetime")?.getattr("timedelta")?; + let adapter = py + .import("pydantic")? + .getattr("TypeAdapter")? + .call1((timedelta,))?; + Ok(adapter + .getattr("validate_python")? + .call1((inner.as_ref(),))? + .unbind()) + } + _ => value.into_py_any(py), + } + }; + let serialize_fn = PyCFunction::new_closure(py, None, None, serialize)?; + + let kwargs = PyDict::new(py); + kwargs.set_item( + "serialization", + core_schema + .getattr("plain_serializer_function_ser_schema")? + .call((serialize_fn,), Some(&serialization_kwargs))?, + )?; + + core_schema + .getattr("no_info_plain_validator_function")? + .call((cls.getattr("_validate")?,), Some(&kwargs)) + } +} diff --git a/duper-python/tests/test_pydantic.py b/duper-python/tests/test_pydantic.py index 3d56b92..4864f22 100644 --- a/duper-python/tests/test_pydantic.py +++ b/duper-python/tests/test_pydantic.py @@ -19,7 +19,7 @@ from uuid import UUID from pydantic import ByteSize -from duper import BaseModel +from duper import BaseModel, TemporalString def test_pydantic_simple(): @@ -81,7 +81,7 @@ def test_pydantic_complex(): class Complex(BaseModel): datetime: datetime duration: timedelta - # zdt: ZonedDateTime + zdt: TemporalString uuid: UUID deque: deque[str] named_tuple: MyTuple @@ -97,9 +97,10 @@ def test_pydantic_complex(): val = Complex( datetime="2025-10-12T20:01:28.400086", duration=timedelta(days=7, seconds=5, microseconds=1), - # zdt=ZonedDateTime.from_string( - # "2022-02-28T11:06:00.092121729+08:00[Asia/Shanghai][u-ca=chinese]" - # ), + zdt=TemporalString( + "2022-02-28T11:06:00.092121729+08:00[Asia/Shanghai][u-ca=chinese]", + type="ZonedDateTime", + ), uuid="a708f86d-ee5b-4ce8-b505-8f59d3d26850", deque=deque(), named_tuple=(34, 35), diff --git a/duper/src/serde/meta.rs b/duper/src/serde/meta.rs index cc95d0d..3ba4e30 100644 --- a/duper/src/serde/meta.rs +++ b/duper/src/serde/meta.rs @@ -8,7 +8,7 @@ use serde_core::{ use crate::{ DuperArray, DuperBytes, DuperIdentifier, DuperInner, DuperKey, DuperObject, DuperString, - DuperTemporal, DuperTemporalInner, DuperTuple, DuperValue, serde::error::DuperSerdeError, + DuperTemporal, DuperTuple, DuperValue, serde::error::DuperSerdeError, }; impl<'a> DuperValue<'a> { @@ -18,71 +18,48 @@ impl<'a> DuperValue<'a> { where S: serde_core::Serializer, { - let mut state = serializer.serialize_struct("DuperValue", 4)?; + let mut state = serializer.serialize_struct("DuperValue", 3)?; state.serialize_field("identifier", &self.identifier)?; match &self.inner { DuperInner::Object(object) => { state.serialize_field("inner", &SerDuperObject(object))?; state.serialize_field("type", "object")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Array(array) => { state.serialize_field("inner", &SerDuperArray(array))?; state.serialize_field("type", "array")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Tuple(tuple) => { state.serialize_field("inner", &SerDuperTuple(tuple))?; state.serialize_field("type", "tuple")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::String(_) => { state.serialize_field("inner", &self.inner)?; state.serialize_field("type", "string")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Bytes(_) => { state.serialize_field("inner", &self.inner)?; state.serialize_field("type", "bytes")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Temporal(temporal) => { - state.serialize_field("inner", &self.inner)?; + state.serialize_field("inner", temporal.as_ref())?; state.serialize_field("type", "temporal")?; - state.serialize_field( - "variant", - &Some(match temporal { - DuperTemporal::Instant(_) => "Instant", - DuperTemporal::ZonedDateTime(_) => "ZonedDateTime", - DuperTemporal::PlainDate(_) => "PlainDate", - DuperTemporal::PlainTime(_) => "PlainTime", - DuperTemporal::PlainDateTime(_) => "PlainDateTime", - DuperTemporal::PlainYearMonth(_) => "PlainYearMonth", - DuperTemporal::PlainMonthDay(_) => "PlainMonthDay", - DuperTemporal::Duration(_) => "Duration", - DuperTemporal::Unspecified(_) => "Unspecified", - }), - )?; } DuperInner::Integer(_) => { state.serialize_field("inner", &self.inner)?; state.serialize_field("type", "integer")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Float(_) => { state.serialize_field("inner", &self.inner)?; state.serialize_field("type", "float")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Boolean(_) => { state.serialize_field("inner", &self.inner)?; state.serialize_field("type", "boolean")?; - state.serialize_field("variant", &Option::<&'static str>::None); } DuperInner::Null => { state.serialize_field("inner", &self.inner)?; state.serialize_field("type", "null")?; - state.serialize_field("variant", &Option::<&'static str>::None); } } state.end() @@ -97,10 +74,7 @@ impl<'a> DuperValue<'a> { 'de: 'a, { let duper_value_meta = DeDuperValue::deserialize(deserializer)?; - Ok(DuperValue { - identifier: duper_value_meta.identifier, - inner: duper_value_meta.inner.try_into().map_err(Error::custom)?, - }) + duper_value_meta.try_into().map_err(Error::custom) } } @@ -165,21 +139,20 @@ struct DeDuperValue<'b> { inner: DeDuperInner<'b>, } -impl<'b> TryFrom> for DuperValue<'b> { - type Error = DuperSerdeError; - - fn try_from(value: DeDuperValue<'b>) -> Result { - Ok(DuperValue { - identifier: value.identifier, - inner: value.inner.try_into()?, - }) - } -} - struct DeDuperObject<'b>(Vec<(DuperKey<'b>, DeDuperValue<'b>)>); struct DeDuperArray<'b>(Vec>); struct DeDuperTuple<'b>(Vec>); -struct DeDuperTemporal<'b>(Cow<'b, str>); +enum DeDuperTemporal<'b> { + Instant(Cow<'b, str>), + ZonedDateTime(Cow<'b, str>), + PlainDate(Cow<'b, str>), + PlainTime(Cow<'b, str>), + PlainDateTime(Cow<'b, str>), + PlainYearMonth(Cow<'b, str>), + PlainMonthDay(Cow<'b, str>), + Duration(Cow<'b, str>), + Unspecified(Cow<'b, str>), +} enum DeDuperInner<'b> { Object(DeDuperObject<'b>), @@ -211,39 +184,90 @@ impl Display for DeDuperInner<'_> { } } -impl<'b> TryFrom> for DuperInner<'b> { +impl<'b> TryFrom> for DuperValue<'b> { type Error = DuperSerdeError; - fn try_from(value: DeDuperInner<'b>) -> Result { - match value { - DeDuperInner::Object(object) => Ok(DuperInner::Object(DuperObject::try_from( - object - .0 - .into_iter() - .map(|(key, value)| DuperValue::try_from(value).map(|value| (key, value))) - .collect::, _>>()?, - )?)), - DeDuperInner::Array(array) => Ok(DuperInner::Array(DuperArray::from( - array - .0 - .into_iter() - .map(DuperValue::try_from) - .collect::, _>>()?, - ))), - DeDuperInner::Tuple(tuple) => Ok(DuperInner::Tuple(DuperTuple::from( - tuple - .0 - .into_iter() - .map(DuperValue::try_from) - .collect::, _>>()?, - ))), - DeDuperInner::String(string) => Ok(DuperInner::String(string)), - DeDuperInner::Bytes(bytes) => Ok(DuperInner::Bytes(bytes)), - DeDuperInner::Temporal(temporal) => Ok(DuperInner::Temporal(temporal)), - DeDuperInner::Integer(integet) => Ok(DuperInner::Integer(integet)), - DeDuperInner::Float(float) => Ok(DuperInner::Float(float)), - DeDuperInner::Boolean(boolean) => Ok(DuperInner::Boolean(boolean)), - DeDuperInner::Null => Ok(DuperInner::Null), + fn try_from(value: DeDuperValue<'b>) -> Result { + let DeDuperValue { identifier, inner } = value; + match inner { + DeDuperInner::Object(object) => Ok(DuperValue { + identifier, + inner: DuperInner::Object(DuperObject::try_from( + object + .0 + .into_iter() + .map(|(key, value)| DuperValue::try_from(value).map(|value| (key, value))) + .collect::, _>>()?, + )?), + }), + DeDuperInner::Array(array) => Ok(DuperValue { + identifier, + inner: DuperInner::Array(DuperArray::from( + array + .0 + .into_iter() + .map(DuperValue::try_from) + .collect::, _>>()?, + )), + }), + DeDuperInner::Tuple(tuple) => Ok(DuperValue { + identifier, + inner: DuperInner::Tuple(DuperTuple::from( + tuple + .0 + .into_iter() + .map(DuperValue::try_from) + .collect::, _>>()?, + )), + }), + DeDuperInner::String(string) => Ok(DuperValue { + identifier, + inner: DuperInner::String(string), + }), + DeDuperInner::Bytes(bytes) => Ok(DuperValue { + identifier, + inner: DuperInner::Bytes(bytes), + }), + DeDuperInner::Temporal(temporal) => Ok(DuperValue { + inner: DuperInner::Temporal(match temporal { + DeDuperTemporal::Instant(inner) => DuperTemporal::try_instant_from(inner)?, + DeDuperTemporal::ZonedDateTime(inner) => { + DuperTemporal::try_zoned_date_time_from(inner)? + } + DeDuperTemporal::PlainDate(inner) => DuperTemporal::try_plain_date_from(inner)?, + DeDuperTemporal::PlainTime(inner) => DuperTemporal::try_plain_time_from(inner)?, + DeDuperTemporal::PlainDateTime(inner) => { + DuperTemporal::try_plain_date_time_from(inner)? + } + DeDuperTemporal::PlainYearMonth(inner) => { + DuperTemporal::try_plain_year_month_from(inner)? + } + DeDuperTemporal::PlainMonthDay(inner) => { + DuperTemporal::try_plain_month_day_from(inner)? + } + DeDuperTemporal::Duration(inner) => DuperTemporal::try_duration_from(inner)?, + DeDuperTemporal::Unspecified(inner) => { + DuperTemporal::try_unspecified_from(inner)? + } + }), + identifier, + }), + DeDuperInner::Integer(integet) => Ok(DuperValue { + identifier, + inner: DuperInner::Integer(integet), + }), + DeDuperInner::Float(float) => Ok(DuperValue { + identifier, + inner: DuperInner::Float(float), + }), + DeDuperInner::Boolean(boolean) => Ok(DuperValue { + identifier, + inner: DuperInner::Boolean(boolean), + }), + DeDuperInner::Null => Ok(DuperValue { + identifier, + inner: DuperInner::Null, + }), } } } @@ -519,7 +543,6 @@ impl<'de> Visitor<'de> for DeDuperValueVisitor { let mut identifier: Option> = None; let mut inner: Option> = None; let mut typ: Option = None; - let mut variant: Option = None; while let Some(key) = map.next_key::()? { match key.as_str() { @@ -548,17 +571,10 @@ impl<'de> Visitor<'de> for DeDuperValueVisitor { &"one of: object, array, tuple, string, bytes, temporal, integer, float, boolean, null", ))?); } - "variant" => { - if variant.is_some() { - return Err(Error::duplicate_field("variant")); - } - let wrapper: Option = map.next_value()?; - variant = wrapper; - } _ => { return Err(Error::unknown_field( key.as_str(), - &["identifier", "inner", "type", "variant"], + &["identifier", "inner", "type"], )); } } @@ -610,20 +626,35 @@ impl<'de> Visitor<'de> for DeDuperValueVisitor { DeDuperInner::String(string) } // Temporal from string - (DeDuperInner::String(string), DeDuperType::Temporal) => { - DeDuperInner::Temporal(match variant.as_ref().map(|string| string.as_str()) { - Some("Instant") => DuperTemporal::Instant( - DuperTemporalInner::try_from(string.into_inner()).map_err(|err| { - Error::custom(format!("failed to parse Temporal value: {err}")) - })?, - ), - Some(_) | None => { - return Err(Error::custom(format!( - "invalid variant '{variant:?}'for Temporal value" - ))); - } - }) - } + (DeDuperInner::String(string), DeDuperType::Temporal) => match &identifier { + Some(ident) if ident.as_ref() == "Instant" => { + DeDuperInner::Temporal(DeDuperTemporal::Instant(string.into_inner())) + } + Some(ident) if ident.as_ref() == "ZonedDateTime" => { + DeDuperInner::Temporal(DeDuperTemporal::ZonedDateTime(string.into_inner())) + } + Some(ident) if ident.as_ref() == "PlainDate" => { + DeDuperInner::Temporal(DeDuperTemporal::PlainDate(string.into_inner())) + } + Some(ident) if ident.as_ref() == "PlainTime" => { + DeDuperInner::Temporal(DeDuperTemporal::PlainTime(string.into_inner())) + } + Some(ident) if ident.as_ref() == "PlainDateTime" => { + DeDuperInner::Temporal(DeDuperTemporal::PlainDateTime(string.into_inner())) + } + Some(ident) if ident.as_ref() == "PlainYearMonth" => { + DeDuperInner::Temporal(DeDuperTemporal::PlainYearMonth(string.into_inner())) + } + Some(ident) if ident.as_ref() == "PlainMonthDay" => { + DeDuperInner::Temporal(DeDuperTemporal::PlainMonthDay(string.into_inner())) + } + Some(ident) if ident.as_ref() == "Duration" => { + DeDuperInner::Temporal(DeDuperTemporal::Duration(string.into_inner())) + } + Some(_) | None => { + DeDuperInner::Temporal(DeDuperTemporal::Unspecified(string.into_inner())) + } + }, // Fallback (inner, typ) => { return Err(Error::custom(format!( @@ -643,7 +674,7 @@ impl<'de> Deserialize<'de> for DeDuperValue<'de> { { deserializer.deserialize_struct( "DuperValue", - &["identifier", "inner", "type", "variant"], + &["identifier", "inner", "type"], DeDuperValueVisitor, ) } @@ -785,13 +816,10 @@ mod serde_meta_tests { ( DuperKey::from("temporal"), DuperValue { - identifier: None, - inner: DuperInner::Temporal(DuperTemporal::Instant( - DuperTemporalInner::try_from(Cow::Borrowed( - "2022-02-28T03:06:00.092121729Z", - )) - .unwrap(), - )), + identifier: Some(DuperIdentifier(Cow::Borrowed("Instant"))), + inner: DuperInner::Temporal(DuperTemporal::Instant(DuperTemporalInner( + Cow::Borrowed("2022-02-28T03:06:00.092121729Z"), + ))), }, ), ( @@ -849,12 +877,9 @@ mod serde_meta_tests { identifier: Some( DuperIdentifier::try_from("MyTemporal").expect("valid identifier"), ), - inner: DuperInner::Temporal(DuperTemporal::Instant( - DuperTemporalInner::try_from(Cow::Borrowed( - "2022-02-28T03:06:00.092121729Z", - )) - .unwrap(), - )), + inner: DuperInner::Temporal(DuperTemporal::Unspecified(DuperTemporalInner( + Cow::Borrowed("2022-02-28T03:06:00.092121729Z"), + ))), }, DuperValue { identifier: Some(DuperIdentifier::try_from("MyInt").expect("valid identifier")), diff --git a/duper/src/serde/mod.rs b/duper/src/serde/mod.rs index ab7ade6..4336d17 100644 --- a/duper/src/serde/mod.rs +++ b/duper/src/serde/mod.rs @@ -1,7 +1,6 @@ pub mod de; pub mod error; -// TO-DO: Temporal - Support meta-serde -// pub mod meta; +pub mod meta; pub mod ser; use std::borrow::Cow; diff --git a/duper/src/serde/snapshots/duper__serde__meta__serde_meta_tests__serialize_scalars.snap b/duper/src/serde/snapshots/duper__serde__meta__serde_meta_tests__serialize_scalars.snap index 2cdd8be..deaa314 100644 --- a/duper/src/serde/snapshots/duper__serde__meta__serde_meta_tests__serialize_scalars.snap +++ b/duper/src/serde/snapshots/duper__serde__meta__serde_meta_tests__serialize_scalars.snap @@ -16,7 +16,7 @@ DuperValue({ type: "bytes", }), temporal: DuperValue({ - identifier: null, + identifier: "Instant", inner: "2022-02-28T03:06:00.092121729Z", type: "temporal", }), -- 2.51.2