diff --git a/README.md b/README.md
index 7393db1..89d5477 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Duper excels in a variety of use cases:
- In configuration files, where users are expected to swap out values, its explicit types can be a helpful guide.
- Thanks to its self-documenting identifiers, Duper feels right at home in REST APIs.
-- With support for bytes and raw data, minimal syntax, and compatibility with JSON, Duper is also a great choice for data interchange.
+- With a simple syntax and extended type support, using Duper for logs is a breath of fresh air for both manual and tool-assisted debugging.
## For implementers
diff --git a/duper-python/pyproject.toml b/duper-python/pyproject.toml
index 85e527b..2cdf839 100644
--- a/duper-python/pyproject.toml
+++ b/duper-python/pyproject.toml
@@ -16,7 +16,9 @@ classifiers = [
"Framework :: Pydantic",
"Framework :: Pydantic :: 2",
]
-dependencies = ["pydantic>=2,<3"]
+dependencies = [
+ "pydantic>=2,<3",
+]
[project.urls]
homepage = "https://duper.dev.br"
diff --git a/duper-python/src/de.rs b/duper-python/src/de.rs
index 625ab4d..d52e8a4 100644
--- a/duper-python/src/de.rs
+++ b/duper-python/src/de.rs
@@ -1,4 +1,4 @@
-use duper::visitor::DuperVisitor;
+use duper::{DuperTemporal, visitor::DuperVisitor};
use pyo3::{prelude::*, types::*};
use crate::Duper;
@@ -124,11 +124,62 @@ impl<'py> DuperVisitor for Visitor<'py> {
fn visit_temporal<'a>(
&mut self,
identifier: Option<&duper::DuperIdentifier<'a>>,
- temporal: &duper::DuperTemporal<'a>,
+ temporal: &DuperTemporal<'a>,
) -> Self::Value {
- // TO-DO: Temporal - Use the appropriate temporal-python type for the DuperTemporal value
Ok(VisitorValue {
- value: PyString::new(self.py, &temporal.clone().into_inner()).into_any(),
+ 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()
+ }
+ },
duper: identifier
.map(|identifier| Duper::from_identifier(identifier)?.into_pyobject(self.py))
.transpose()?,
diff --git a/duper-python/src/ser/well_known_type.rs b/duper-python/src/ser/well_known_type.rs
index 07582bc..d65a51b 100644
--- a/duper-python/src/ser/well_known_type.rs
+++ b/duper-python/src/ser/well_known_type.rs
@@ -90,6 +90,15 @@ 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>),
// uuid
Uuid(Bound<'py, PyAny>),
}
@@ -298,6 +307,31 @@ 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()))),
_ => (),
@@ -341,29 +375,71 @@ impl<'py> WellKnownType<'py> {
inner: serialize_pyany(value.getattr("__dict__")?)?.inner,
}),
// datetime
- WellKnownType::DateTime(value) => Ok(DuperValue {
- identifier: Some(
- DuperIdentifier::try_from(Cow::Borrowed("DateTime")).expect("valid identifier"),
- ),
- inner: DuperInner::Temporal(
- DuperTemporal::try_instant_from(Cow::Owned(
- value.call_method0("isoformat")?.extract()?,
- ))
- .map_err(|err| {
- PyValueError::new_err(format!("Failed to parse Temporal value: {err}"))
- })?,
- ),
+ WellKnownType::DateTime(value) => Ok({
+ let datetime: Cow<'_, str> =
+ Cow::Owned(value.call_method0("isoformat")?.extract()?);
+ let tzinfo = value.getattr("tzinfo")?;
+ if tzinfo.is_none() || tzinfo.getattr("utcoffset")?.call1((value,))?.is_none() {
+ DuperValue {
+ identifier: Some(
+ DuperIdentifier::try_from(Cow::Borrowed("PlainDateTime"))
+ .expect("valid identifier"),
+ ),
+ inner: DuperInner::Temporal(
+ DuperTemporal::try_plain_date_time_from(datetime).map_err(|err| {
+ PyValueError::new_err(format!(
+ "Failed to parse Temporal value: {err}"
+ ))
+ })?,
+ ),
+ }
+ } else {
+ DuperValue {
+ identifier: Some(
+ DuperIdentifier::try_from(Cow::Borrowed("Instant"))
+ .expect("valid identifier"),
+ ),
+ inner: DuperInner::Temporal(
+ DuperTemporal::try_instant_from(datetime).map_err(|err| {
+ PyValueError::new_err(format!(
+ "Failed to parse Temporal value: {err}"
+ ))
+ })?,
+ ),
+ }
+ }
}),
WellKnownType::TimeDelta(value) => Ok(DuperValue {
identifier: Some(
- DuperIdentifier::try_from(Cow::Borrowed("TimeDelta"))
- .expect("valid identifier"),
- ),
- inner: DuperInner::String(DuperString::from(Cow::Owned(value.str()?.extract()?))),
+ DuperIdentifier::try_from(Cow::Borrowed("Duration")).expect("valid identifier"),
+ ),
+ inner: {
+ let py = value.py();
+ let datetime_module = py.import("datetime")?;
+ let pydantic_module = py.import("pydantic")?;
+ let adapter = pydantic_module
+ .getattr("TypeAdapter")?
+ .call1((datetime_module.getattr("timedelta")?,))?;
+ let kwargs = PyDict::new(py);
+ kwargs.set_item("mode", "json")?;
+ let dump = adapter
+ .getattr("dump_python")?
+ .call((value,), Some(&kwargs))?;
+ DuperInner::Temporal(
+ DuperTemporal::try_duration_from(Cow::Owned(dump.extract()?)).map_err(
+ |err| {
+ PyValueError::new_err(format!(
+ "Failed to parse Temporal value: {err}"
+ ))
+ },
+ )?,
+ )
+ },
}),
WellKnownType::Date(value) => Ok(DuperValue {
identifier: Some(
- DuperIdentifier::try_from(Cow::Borrowed("Date")).expect("valid identifier"),
+ DuperIdentifier::try_from(Cow::Borrowed("PlainDate"))
+ .expect("valid identifier"),
),
inner: DuperInner::Temporal(
DuperTemporal::try_plain_date_from(Cow::Owned(
@@ -376,7 +452,8 @@ impl<'py> WellKnownType<'py> {
}),
WellKnownType::Time(value) => Ok(DuperValue {
identifier: Some(
- DuperIdentifier::try_from(Cow::Borrowed("Time")).expect("valid identifier"),
+ DuperIdentifier::try_from(Cow::Borrowed("PlainTime"))
+ .expect("valid identifier"),
),
inner: DuperInner::Temporal(
DuperTemporal::try_plain_time_from(Cow::Owned(
@@ -767,6 +844,103 @@ impl<'py> WellKnownType<'py> {
value.getattr("pattern")?.extract()?,
))),
}),
+ // temporal
+ WellKnownType::TemporalInstant(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalZonedDateTime(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalPlainDate(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalPlainTime(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalPlainDateTime(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalPlainYearMonth(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalPlainMonthDay(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}"))
+ })?,
+ ),
+ }),
+ WellKnownType::TemporalDuration(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}"))
+ })?,
+ ),
+ }),
// uuid
WellKnownType::Uuid(value) => Ok(DuperValue {
identifier: Some(
diff --git a/duper-python/tests/test_basic.py b/duper-python/tests/test_basic.py
index 1de5987..e16f998 100644
--- a/duper-python/tests/test_basic.py
+++ b/duper-python/tests/test_basic.py
@@ -2,7 +2,8 @@ import pytest
import duper
-DUPER_DATA = """APIResponse({
+DUPER_DATA = """
+APIResponse({
status: 200,
headers: {
content_type: "application/duper",
@@ -41,7 +42,7 @@ def test_basic():
assert (
duper.dumps(obj, strip_identifiers=True)
- == r"""{status: 200, headers: {content_type: "application/duper", cache_control: "max-age=3600"}, body: {users: [{id: "7039311b-02d2-4849-a6de-900d4dbe9acb", name: "Alice", email: "alice@example.com", roles: ["admin", "user"], metadata: {last_login: "2024-01-15T10:30:00Z", ip: "173.255.230.79"}}]}}"""
+ == r"""{status: 200, headers: {content_type: "application/duper", cache_control: "max-age=3600"}, body: {users: [{id: "7039311b-02d2-4849-a6de-900d4dbe9acb", name: "Alice", email: "alice@example.com", roles: ["admin", "user"], metadata: {last_login: '2024-01-15T10:30:00Z', ip: "173.255.230.79"}}]}}"""
)
assert (
@@ -51,7 +52,7 @@ def test_basic():
assert (
duper.dumps(obj, strip_identifiers=True, minify=True)
- == r"""{status:200,headers:{content_type:"application/duper",cache_control:"max-age=3600"},body:{users:[{id:"7039311b-02d2-4849-a6de-900d4dbe9acb",name:"Alice",email:"alice@example.com",roles:["admin","user"],metadata:{last_login:"2024-01-15T10:30:00Z",ip:"173.255.230.79"}}]}}"""
+ == r"""{status:200,headers:{content_type:"application/duper",cache_control:"max-age=3600"},body:{users:[{id:"7039311b-02d2-4849-a6de-900d4dbe9acb",name:"Alice",email:"alice@example.com",roles:["admin","user"],metadata:{last_login:'2024-01-15T10:30:00Z',ip:"173.255.230.79"}}]}}"""
)
with pytest.raises(
diff --git a/duper-python/tests/test_pydantic.py b/duper-python/tests/test_pydantic.py
index c9a68a9..3d56b92 100644
--- a/duper-python/tests/test_pydantic.py
+++ b/duper-python/tests/test_pydantic.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from collections import deque
-from datetime import datetime
+from datetime import datetime, timedelta
from decimal import Decimal
from enum import Enum
from ipaddress import (
@@ -75,11 +75,13 @@ def test_pydantic_complex():
@dataclass
class Regex:
- pattern: re.Pattern
+ pattern: re.Pattern[str]
matches: list[str] | None = None
class Complex(BaseModel):
datetime: datetime
+ duration: timedelta
+ # zdt: ZonedDateTime
uuid: UUID
deque: deque[str]
named_tuple: MyTuple
@@ -94,6 +96,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]"
+ # ),
uuid="a708f86d-ee5b-4ce8-b505-8f59d3d26850",
deque=deque(),
named_tuple=(34, 35),
@@ -118,7 +124,7 @@ def test_pydantic_complex():
assert (
val_dump
- == """Complex({datetime: DateTime('2025-10-12T20:01:28.400086'), uuid: Uuid("a708f86d-ee5b-4ce8-b505-8f59d3d26850"), deque: Deque([]), named_tuple: (34, 35), set: Set([1, 2, 4]), bytesize: ByteSize(3072000), decimal: Decimal("12.34"), enum: IPv4Address(2), typeddict: {x: 1, y: 2, label: "good"}, path: PosixPath("/dev/null"), regex: Regex({pattern: Pattern("^Hello w.rld!$"), matches: null}), sub: Submodel({address4: IPv4Address("192.168.0.1"), interface4: IPv4Interface("192.168.0.2/32"), network4: IPv4Network("192.168.0.0/24"), address6: IPv6Address("2001:db8::1"), interface6: IPv6Interface("2001:db8::2/128"), network6: IPv6Network("2001:db8::/128")})})"""
+ == """Complex({datetime: PlainDateTime('2025-10-12T20:01:28.400086'), duration: Duration('P7DT5.000001S'), zdt: ZonedDateTime('2022-02-28T11:06:00.092121729+08:00[Asia/Shanghai][u-ca=chinese]'), uuid: Uuid("a708f86d-ee5b-4ce8-b505-8f59d3d26850"), deque: Deque([]), named_tuple: (34, 35), set: Set([1, 2, 4]), bytesize: ByteSize(3072000), decimal: Decimal("12.34"), enum: IPv4Address(2), typeddict: {x: 1, y: 2, label: "good"}, path: PosixPath("/dev/null"), regex: Regex({pattern: Pattern("^Hello w.rld!$"), matches: null}), sub: Submodel({address4: IPv4Address("192.168.0.1"), interface4: IPv4Interface("192.168.0.2/32"), network4: IPv4Network("192.168.0.0/24"), address6: IPv6Address("2001:db8::1"), interface6: IPv6Interface("2001:db8::2/128"), network6: IPv6Network("2001:db8::/128")})})"""
)
val2 = Complex.model_validate_duper(val_dump)
diff --git a/duper-vs-code/syntaxes/duper.tmLanguage.json b/duper-vs-code/syntaxes/duper.tmLanguage.json
index f74d654..4c7c406 100644
--- a/duper-vs-code/syntaxes/duper.tmLanguage.json
+++ b/duper-vs-code/syntaxes/duper.tmLanguage.json
@@ -23,7 +23,7 @@
"strings": {
"patterns": [
{
- "name": "string.quoted.double.raw_one.duper",
+ "name": "string.quoted.double.raw.duper",
"begin": "b?r(#*)\"",
"beginCaptures": {
"1": {
@@ -37,6 +37,17 @@
}
}
},
+ {
+ "name": "string.quoted.base64.duper",
+ "begin": "b64\"",
+ "end": "\"",
+ "patterns": [
+ {
+ "name": "string.quoted.base64content.duper",
+ "match": "\\s*[A-Za-z0-9+/=]+\\s*"
+ }
+ ]
+ },
{
"name": "string.quoted.double.duper",
"begin": "b?\"",
@@ -46,17 +57,21 @@
"include": "#stringescapes"
},
{
- "include": "#stringcontent"
+ "match": ".",
+ "name": "string.quoted.content.duper"
}
]
- }
- ]
- },
- "stringcontent": {
- "patterns": [
+ },
{
- "match": ".",
- "name": "string.quoted.double.duper"
+ "name": "string.single.temporal.duper",
+ "begin": "'",
+ "end": "'",
+ "patterns": [
+ {
+ "match": ".",
+ "name": "string.single.content.duper"
+ }
+ ]
}
]
},
diff --git a/duper/src/format.rs b/duper/src/format.rs
index 7036f49..0361bc7 100644
--- a/duper/src/format.rs
+++ b/duper/src/format.rs
@@ -159,44 +159,11 @@ pub(crate) fn format_duper_bytes<'a>(bytes: &'a DuperBytes<'a>) -> Cow<'a, str>
}
pub(crate) fn format_temporal<'a>(temporal: &'a DuperTemporal<'a>) -> String {
- match temporal {
- DuperTemporal::Instant(inner) => {
- let value = inner.as_ref();
- format!("Instant('{value}')")
- }
- DuperTemporal::ZonedDateTime(inner) => {
- let value = inner.as_ref();
- format!("ZonedDateTime('{value}')")
- }
- DuperTemporal::PlainDate(inner) => {
- let value = inner.as_ref();
- format!("PlainDate('{value}')")
- }
- DuperTemporal::PlainTime(inner) => {
- let value = inner.as_ref();
- format!("PlainTime('{value}')")
- }
- DuperTemporal::PlainDateTime(inner) => {
- let value = inner.as_ref();
- format!("PlainDateTime('{value}')")
- }
- DuperTemporal::PlainYearMonth(inner) => {
- let value = inner.as_ref();
- format!("PlainYearMonth('{value}')")
- }
- DuperTemporal::PlainMonthDay(inner) => {
- let value = inner.as_ref();
- format!("PlainMonthDay('{value}')")
- }
- DuperTemporal::Duration(inner) => {
- let value = inner.as_ref();
- format!("Duration('{value}')")
- }
- DuperTemporal::Unspecified(inner) => {
- let value = inner.as_ref();
- format!("'{value}'")
- }
- }
+ let mut string = String::with_capacity(temporal.as_ref().len() + 2);
+ string.push('\'');
+ string.push_str(temporal.as_ref());
+ string.push('\'');
+ string
}
pub(crate) fn format_integer(integer: i64) -> String {
diff --git a/duper_website/docs/index.md b/duper_website/docs/index.md
index 5d7a6e8..314dd47 100644
--- a/duper_website/docs/index.md
+++ b/duper_website/docs/index.md
@@ -57,9 +57,9 @@ const initial = `UserProfile({
Duper excels in a variety of use cases:
-- **Configuration files**: Duper's explicit types and identifiers serve as helpful guides when users need to modify values.
+- **Configuration files**: Duper's explicit types and comments serve as helpful guides when users need to modify values.
- **REST APIs**: Self-documenting identifiers make Duper a natural fit for API responses.
-- **Data interchange**: With support for bytes, raw data, JSON compatibility, and identifiers that can be turned on for debugging-only, Duper is ideal for data exchange between systems.
+- **Logging**: With a simple syntax and extended type support, Duper is a breath of fresh air for both manual and tool-assisted debugging.
## Playground
@@ -79,7 +79,7 @@ Duper excels in a variety of use cases:
| Raw strings | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| Identifiers/tags | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
| Unambiguous | ✅ | ✅ | ✅ | ❌\[3] | ⚠️\[4] | ✅ |
-| Simple | ⚠️\[2] | ✅ | ✅ | ❌ | ✅ | ✅ |
+| Simple | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| JSON-compatible | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Popular | ❌ | ✅ | ⚠️ | ✅ | ✅ | ⚠️ |
diff --git a/duper_website/docs/quick-start.md b/duper_website/docs/quick-start.md
index b916ffd..5d3678e 100644
--- a/duper_website/docs/quick-start.md
+++ b/duper_website/docs/quick-start.md
@@ -1,6 +1,6 @@
# Quick start
-## JavaScript
+## JavaScript (WebAssembly)
```bash
npm install --save @duper-js/wasm
diff --git a/duper_website/docs/spec.md b/duper_website/docs/spec.md
index d73fe26..a2fca85 100644
--- a/duper_website/docs/spec.md
+++ b/duper_website/docs/spec.md
@@ -269,13 +269,22 @@ Byte strings are similar to strings, but represent binary data. Like strings, th
## Temporal values
-Temporal values are a set of value types, representing a point in time or the difference between two points in time. They are surrounded by single quotes `'` and must follow the [Temporal proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), which uses the format specified in [RFC 9557](https://datatracker.ietf.org/doc/html/rfc9557).
+Temporal values are a set of value types, representing either a point in time or the difference between two points in time. They are surrounded by single quotes `'` and must follow the [Temporal proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), which uses a strict version of the format specified in [RFC 9557](https://datatracker.ietf.org/doc/html/rfc9557) (itself based off of ISO 8601 / RFC 3339).
-Whitespace between the Temporal value and the single quotes is allowed and ignored. Parsers should validate that the value between single quotes is a valid Temporal value.
+Whitespace between the Temporal value and the single quotes is allowed and ignored. Parsers must validate that the value between single quotes is a valid Temporal value.
```duper
{
+ // Allowed
+ instant: '2022-02-28T03:06:00.092121729Z',
+ duration: ' P7DT5.000001S ',
+ calendar_system: '2020-05-22[u-ca=hebrew]', // 28 Iyar 5780
+ christmas_eve: '--12-24',
+ large_extensions: '2020-05-22T07:19:35.123456789-04:00[America/Indiana/Indianapolis][u-ca=islamic-umalqura]',
+ // Not allowed
+ not_temporal: 'hello world', // INVALID
+ "date doesn't exist": '2025-02-29', // INVALID
}
```
@@ -292,7 +301,18 @@ These values may or may not contain an [identifier](#identifiers). In the case w
```duper
{
+ // Allowed
+ precise_identifier: PlainDateTime('2007-03-31T10:35:10'),
+ subset: PlainYearMonth('1994-11-06T19:45:27-03:00'), // PlainYearMonth is a subset of Instant
+ // Allowed but discouraged
+ string_in_disguise: PlainDate("this isn't a Temporal value"),
+ confusing_identifier: PlainTimeDate('2025-11-03'), /* Unlike `PlainDateTime`, this doesn't
+ * validate the input, other than that
+ * it's a Temporal value. */
+
+ // Not allowed
+ missing_offset: Instant('2025-11-06T19:39:02.888834'), // INVALID
}
```