diff --git a/SPEC.md b/SPEC.md
index 3117cdc..8ba8258 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -4,7 +4,7 @@
Duper: The format that's super!
-
+
@@ -381,7 +381,7 @@ Any parenthesized expression must be interpreted as a tuple by parsers.
Identifiers are optional type-like annotations that wrap any kind of value, providing semantic meaning or hinting at special handling during parsing/validation. Identified values are composed of the identifier name, followed by the value wrapped in parenthesis `(` and `)`.
-The first character must be an uppercase letter, followed by letters, numbers, underscores `_`, and hyphens `-`. They must start with an ASCII letter, or an underscore followed by a letter or digit. Sequences of underscores and hyphens are not allowed in the identifier, and identifiers may not start or end with either.
+The first character must be an ASCII uppercase letter, followed by ASCII letters, ASCII digits, underscores `_`, and hyphens `-`. Sequences of underscores and hyphens are not allowed in the identifier, and identifiers may not start or end with either of them.
```duper
{
diff --git a/duper-python/pyproject.toml b/duper-python/pyproject.toml
index 5063ae4..cb3df30 100644
--- a/duper-python/pyproject.toml
+++ b/duper-python/pyproject.toml
@@ -17,9 +17,7 @@ classifiers = [
"Framework :: Pydantic :: 2",
]
urls = { homepage = "https://github.com/EpicEric/duper", source = "https://github.com/EpicEric/duper" }
-dependencies = [
- "pydantic>=2.12.0",
-]
+dependencies = ["pydantic>=2,<3"]
[build-system]
requires = ["maturin>=1.9,<2.0"]
@@ -36,6 +34,7 @@ dev = [
"fastapi[standard]>=0.119.0",
"httpx>=0.28.1",
"maturin>=1.9.6",
+ "pydantic-extra-types[all]>=2.10.6",
"pytest>=8.4.2",
"ruff>=0.14.1",
"starlette>=0.48.0",
diff --git a/duper-python/src/de.rs b/duper-python/src/de.rs
index bb8105e..2f4ec9f 100644
--- a/duper-python/src/de.rs
+++ b/duper-python/src/de.rs
@@ -46,7 +46,7 @@ impl<'py> DuperVisitor for Visitor<'py> {
let model = pydantic.getattr("create_model")?.call(
(identifier
.map(|identifier| identifier.as_ref())
- .unwrap_or("DuperUnknown"),),
+ .unwrap_or("Unknown"),),
Some(&model_fields),
)?;
Ok(VisitorValue {
diff --git a/duper-python/src/ser.rs b/duper-python/src/ser.rs
deleted file mode 100644
index c4f2349..0000000
--- a/duper-python/src/ser.rs
+++ /dev/null
@@ -1,574 +0,0 @@
-use std::borrow::Cow;
-
-use duper::{
- DuperArray, DuperBytes, DuperIdentifier, DuperInner, DuperKey, DuperObject, DuperString,
- DuperTuple, DuperValue,
-};
-use pyo3::{BoundObject, exceptions::PyValueError, prelude::*, types::*};
-
-use crate::Duper;
-
-pub(crate) fn serialize_pyany<'py>(obj: Bound<'py, PyAny>) -> PyResult> {
- // Handle basic types
- if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Object(
- DuperObject::try_from(serialize_pydict(obj.cast()?)?)
- .expect("no duplicate keys in dict"),
- ),
- })
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Array(DuperArray::from(serialize_pyiter(obj.try_iter()?)?)),
- })
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: Some(
- DuperIdentifier::try_from(Cow::Borrowed("Set")).expect("valid identifier"),
- ),
- inner: DuperInner::Array(DuperArray::from(serialize_pyiter(obj.try_iter()?)?)),
- })
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Tuple(DuperTuple::from(serialize_pyiter(obj.try_iter()?)?)),
- })
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Bytes(DuperBytes::from(Cow::Owned(obj.extract()?))),
- })
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::String(DuperString::from(Cow::Owned(obj.extract()?))),
- })
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Boolean(obj.extract()?),
- })
- } else if obj.is_instance_of::() {
- let identifier = {
- let identifier = serialize_pyclass_identifier(&obj)?;
- if identifier
- .as_ref()
- .is_some_and(|identifier| identifier.as_ref() != "Int")
- {
- identifier
- } else {
- None
- }
- };
- if let Ok(integer) = obj.extract() {
- Ok(DuperValue {
- identifier,
- inner: DuperInner::Integer(integer),
- })
- } else {
- Ok(DuperValue {
- identifier: identifier.or(Some(
- DuperIdentifier::try_from(Cow::Borrowed("Int")).expect("valid identifier"),
- )),
- inner: DuperInner::String(DuperString::from(Cow::Owned(obj.str()?.extract()?))),
- })
- }
- } else if obj.is_instance_of::() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Float(obj.extract()?),
- })
- } else if obj.is_none() {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Null,
- })
- }
- // Handle well-known types
- else if let Some(well_known_type) = WellKnownType::identify(&obj)? {
- Ok(well_known_type.serialize()?)
- }
- // Handle sequences
- else if let Ok(pyiter) = obj.try_iter() {
- let identifier = serialize_pyclass_identifier(&obj)?;
- Ok(DuperValue {
- identifier,
- inner: DuperInner::Array(DuperArray::from(serialize_pyiter(pyiter.into_bound())?)),
- })
- }
- // Handle unknown types
- else if obj.hasattr("__bytes__")?
- && let Ok(bytes) = obj
- .call_method0("__bytes__")
- .and_then(|bytes| bytes.extract())
- {
- let identifier = serialize_pyclass_identifier(&obj)?;
- Ok(DuperValue {
- identifier,
- inner: DuperInner::Bytes(DuperBytes::from(Cow::Owned(bytes))),
- })
- } else if obj.hasattr("__slots__")?
- && let Ok(object) = serialize_pyslots(&obj)
- {
- Ok(DuperValue {
- identifier: None,
- inner: DuperInner::Object(
- DuperObject::try_from(object).expect("no duplicate keys in slots"),
- ),
- })
- } else {
- Err(PyErr::new::(format!(
- "Unsupported type: {}",
- obj.get_type()
- )))
- }
-}
-
-fn serialize_pydict<'py>(
- dict: &Bound<'py, PyDict>,
-) -> PyResult, DuperValue<'py>)>> {
- dict.iter()
- .map(|(key, value)| {
- let key: &Bound<'py, PyString> = key.cast()?;
- Ok((
- DuperKey::from(Cow::Owned(key.to_string())),
- serialize_pyany(value)?,
- ))
- })
- .collect()
-}
-
-fn serialize_pyiter<'py>(iterator: Bound<'py, PyIterator>) -> PyResult>> {
- iterator.map(|value| serialize_pyany(value?)).collect()
-}
-
-fn serialize_pyslots<'py>(
- obj: &Bound<'py, PyAny>,
-) -> PyResult, DuperValue<'py>)>> {
- obj.getattr("__slots__")?
- .try_iter()?
- .map(|key: PyResult>| {
- let key = key?;
- let key: &Bound<'py, PyString> = key.cast()?;
- let value = obj.getattr(key)?;
- Ok((
- DuperKey::from(Cow::Owned(key.to_string())),
- serialize_pyany(value)?,
- ))
- })
- .collect()
-}
-
-#[derive(Debug)]
-#[non_exhaustive]
-enum WellKnownType<'py> {
- // collections
- Deque(Bound<'py, PyAny>),
- // dataclasses
- Dataclass(Bound<'py, PyAny>),
- // datetime
- DateTime(Bound<'py, PyAny>),
- TimeDelta(Bound<'py, PyAny>),
- Date(Bound<'py, PyAny>),
- Time(Bound<'py, PyAny>),
- // decimal
- Decimal(Bound<'py, PyAny>),
- // enum
- Enum(Bound<'py, PyAny>),
- // ipaddress
- IPv4Address(Bound<'py, PyAny>),
- IPv4Interface(Bound<'py, PyAny>),
- IPv4Network(Bound<'py, PyAny>),
- IPv6Address(Bound<'py, PyAny>),
- IPv6Interface(Bound<'py, PyAny>),
- IPv6Network(Bound<'py, PyAny>),
- // pathlib
- Path(Bound<'py, PyAny>),
- PosixPath(Bound<'py, PyAny>),
- WindowsPath(Bound<'py, PyAny>),
- PurePath(Bound<'py, PyAny>),
- PurePosixPath(Bound<'py, PyAny>),
- PureWindowsPath(Bound<'py, PyAny>),
- // pydantic
- BaseModel(Bound<'py, PyAny>),
- ByteSize(Bound<'py, PyAny>),
- // re
- Pattern(Bound<'py, PyAny>),
- // uuid
- Uuid(Bound<'py, PyAny>),
-}
-
-impl<'py> WellKnownType<'py> {
- fn identify(value: &Bound<'py, PyAny>) -> PyResult