Imports a Design Tokens Community Group JSON document into a `Config`
through a new public `plumb_config::merge_dtcg(&mut Config, &DtcgSource)`
entry point. Callers own filesystem reads; the adapter is a pure
function of `(input bytes, into.snapshot)`.
Mapped `$type` values:
* `color` → `ColorSpec.tokens` (hex `#rgb` / `#rrggbbaa`)
* `dimension` → `SpacingSpec.tokens` or `TypeScaleSpec.tokens`
via a parent-namespace heuristic
(`typography` / `type` / `font-size` / `text` /
`font` / `size` route to typography; everything
else routes to spacing)
* `fontFamily` → `TypeScaleSpec.families` (deduped, insertion-order)
* `fontWeight` → `TypeScaleSpec.weights`
* `radius` / `borderRadius` → `RadiusSpec.scale`
* `shadow`, `duration`, `cubicBezier`, etc. → `DtcgWarning::UnsupportedType`
(no hard error — the tokens are dropped and reported)
Alias resolution is a single forward pass with cycle detection. Both
DTCG forms are accepted: the brace shorthand `"{path.to.token}"` and
the JSON-Pointer object `{ "$ref": "#/path/to/token" }`. Cycles raise
`ConfigError::DtcgAlias` with the visit order so the failing edge is
human-readable; dangling references raise the same variant with a
single-element cycle naming the missing path.
Defensive parsing for untrusted input: tree depth is capped at 256
levels before walking, malformed JSON returns `ConfigError::DtcgParse`
with a miette `NamedSource`, hex colors reuse the existing
`is_valid_hex_color` helper.
Three fixtures under `tests/fixtures/dtcg/`:
* `flat-palette.json` — bare color palette.
* `nested-aliases.json` — multi-section file (color, spacing,
typography, radius, shadow) with brace
aliases across groups.
* `multi-mode.json` — `$extensions.modes` payload; the canonical
`$value` imports, alternate modes surface as
`MultiMode` warnings.
Twelve integration tests in `tests/dtcg_adapter.rs` plus eight unit
tests cover the happy paths, alias cycles, dangling refs, malformed
JSON, depth-limit enforcement, invalid hex colors, multi-mode handling,
duplicate names, and the `{path}` ↔ `$ref` parity.
The `merge_dtcg` API is internal to `plumb-config`; it does not flow
through `Config` serialization, so the JSON Schema emitted by
`cargo xtask schema` is byte-identical.
Fixes #28
Spec
Fixes #28
Summary
plumb_config::merge_dtcg(&mut Config, &DtcgSource)— a Design Tokens Community Group (DTCG) JSON importer that maps tokens ontoColorSpec,SpacingSpec,TypeScaleSpec, andRadiusSpec, resolves{path.to.token}brace aliases and{ "$ref": "#/..." }JSON-Pointer references with cycle detection, caps tree depth at 256 levels for untrusted input, and reuses the existingis_valid_hex_colorhelper from PR feat(config): span-annotated validation errors via miette #110.$typevalues, multi-mode siblings, duplicate token names) as a typedDtcgWarninglist rather than failing the import.flat-palette.json,nested-aliases.json,multi-mode.json) plus 12 integration tests and 8 unit tests cover the spec examples, the alias forms, and the security-sensitive depth/type/hex guards.Test plan
just checkpasses (fmt + clippy, no warnings)just testpasses on my machinejust determinism-checkpassescargo deny checkpassesdocs/src/**, rustdoc, CHANGELOG if user-visible)docs/src/**prose (nodocs/src/**changes in this PR)Screenshots / terminal output
n/a —
merge_dtcgis a library API, not a user-visible CLI surface.Breaking change?
The new symbols are additive (
DtcgSource,DtcgImport,DtcgWarning,DtcgWarningKind,MAX_NESTING,merge_dtcg) and two newConfigErrorvariants (DtcgParse,DtcgAlias). The existingConfigErrorenum was already#[non_exhaustive], so adding variants is non-breaking.Anything reviewers should double-check
$type: "dimension"tokens go toSpacingSpec.tokensunless the path contains one oftypography/type/font-size/text/font/size, in which case they go toTypeScaleSpec.tokens. The list is documented in the rustdoc ondtcg.rs. Open to expanding the typography keys if Tokens Studio exports name them differently.pxdimensions are accepted.rem/emraiseUnconvertiblewarnings — there's no DPI/root-size context inConfig, so converting silently would be misleading.$valuealways wins; mode payloads (DTCG$extensions.modes) raiseMultiModewarnings rather than overwriting. Plumb does not yet model design-token modes; this keeps imports loss-tolerant rather than ambiguous.ConfigError::DtcgAliascarries the visit order. Single-element cycles encode dangling references — the missing path is the only entry. Two-or-more entries are real cycles.MAX_NESTING = 256is enforced in a separate pass afterserde_jsonhas already accepted the document; this is belt-and-braces becauseserde_json's default recursion limit is 128 and not configurable per-call. Hex colors go through the existingis_valid_hex_colorso a DTCG file cannot smuggle in a non-hex string.merge_dtcgis internal toplumb-config; nothing flows throughConfigserialization.cargo xtask schemaproduces byte-identical output, verified locally.CI checkboxes
indexmaptoplumb-config/Cargo.toml(already a workspace dep used byplumb-core); no new external deps.