Skip to content

tdbin encode/decode ignore generation config — codecs don't compile against configured field types #64

Description

@MelbourneDeveloper

Problem

typediagram encode emits types and codecs together, from the raw schema, with no configuration hook. For any consumer that post-processes --to rust output, that produces a second, divergent definition of every wire type.

Reproduction:

$ cat mini.td
typeDiagram

type CacheStats {
  hits: Int
  misses: Int
}

$ typediagram encode mini.td
/// The `CacheStats` record.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CacheStats {
    /// The `hits` field.
    pub hits: i64,
    /// The `misses` field.
    pub misses: i64,
}

impl tdbin::Struct for CacheStats { … }

Note what is missing: no serde derives, no #[serde(rename_all)], no field-level attributes, no doc text from the schema comments. --to rust output plus our post-processor produces a different CacheStats. Both cannot coexist in one crate.

typediagram decode correctly emits codec impls only, which is the right shape — but it still assumes the type it is generating against is the unmodified generated type. That assumption breaks the moment a field's Rust type is overridden.

The concrete failure: opaque JSON fields

Several of our fields carry arbitrary JSON. The DSL has no type for that (#28), so the schema declares String and our generator overrides the emitted Rust type:

// scripts/typediagram-gen/type-config-core.mjs
JsonRpcResponse: { fieldTypes: { result: "serde_json::Value" } },
JsonRpcRequest:  { fieldTypes: { params: "Option<serde_json::Value>" } },
JsonRpcError:    { fieldTypes: { data:   "Option<serde_json::Value>" } },
MergePlan:       { fieldTypes: { workspace_edit: "Option<serde_json::Value>" } },

decode will emit w.string(at, …, self.params.as_deref()) for those fields. Option<serde_json::Value> has no as_deref. The generated codec does not compile, and there is no flag to tell it otherwise.

The same class of breakage applies to our other overrides:

  • serdeAttrs: ["untagged"] + tupleVariants: ["Number", "String"] on RequestId — the codec is generated against struct-form variants that no longer exist after post-processing.
  • variantDiscriminants pinning ErrorCode to the JSON-RPC integer codes (-32700 … -32001) — a codec that assigns its own dense discriminants silently disagrees with the wire contract.

Ask

  1. Separate type emission from codec emission cleanly. decode should generate codecs against a described type, not a re-derived one — i.e. it must accept the same configuration that shaped --to rust.
  2. Honour a generation config in the tdbin path. Whatever mechanism lands for Critical: user-supplied code TEMPLATE for emitted ADTs (readonly, null/undefined, comments, serde) #51 (user-supplied template) and [CRITICAL] Express field metadata (constraints, defaults, descriptions, aliases, config) so generated DTOs need zero hand-maintenance #39 (field metadata) should apply to encode/decode, not just --to.
  3. Native opaque-payload type (Add 'Any' / 'Json' field type for opaque payloads #28). A first-class Json / Any field type solves this properly for both the JSON path and tdbin: on the tdbin side it can encode as Bytes (a pointer to a UTF-8 or CBOR blob) with no schema-layout consequences. This is the single highest-leverage fix — it removes the need for four of our overrides entirely.
  4. Fail loudly, not at rustc. If a configured field type is not representable, verify should say so, with the field name, rather than emitting code that fails to compile downstream.

Related: #28 (opaque payload type), #39 (field metadata), #51 (emitted-ADT template), #30 (pinned enum discriminants).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions