Summary
The hand-written fluent API (~2000 LOC of BuildNode methods, encoder presets, enum converters, DTO serialization) mirrors types defined in the native imageflow Rust source. As Imageflow 3 drastically expands the API surface, maintaining hand-written parity is untenable. We need schema-driven code generation for versioned namespaces (Imageflow.Fluent.V2, Imageflow.Fluent.V3).
Architecture
Two schema sources
V2 (current API, OpenAPI): The native library embeds an OpenAPI 3.1 schema (94 component schemas) accessible via imageflow_context_send_json("v1/schema/openapi/latest/get", "{}"). The Node enum is a oneOf with serde-style tagged variants. EncoderPreset is similar. All types derive Serialize/Deserialize + utoipa ToSchema.
V3 (new API, zennode): The zennode metadata system provides much richer per-node information than OpenAPI alone:
NodeSchema: id, label, description, group, role, params, tags, version, compat_version, json_key, coalesce info, format preferences
ParamDesc: name, label, description, kind (11 types: Float/Int/U32/Bool/Str/Enum/FloatArray/Color/Json/Object/TaggedUnion), unit, section, slider mapping, kv_keys (RIAPI aliases), since_version, visible_when, optional, json_name, json_aliases
ParamKind: carries min/max/default/identity/step for numerics, variants for enums, sub-params for objects, tagged variants for unions
NodeRole: Decode, Geometry, Orient, Resize, Filter, Composite, Analysis, Encode, Meta, Canvas
NodeGroup: Decode, Encode, Tone, ToneRange, ToneMap, Color, Detail, Effects, Geometry, Layout, Canvas, Composite, Quantize, Analysis, Hdr, Raw, Auto, Other
Each zen crate (zenfilters, zenresize, etc.) defines nodes via #[derive(Node)] with rich annotations:
#[derive(Node, Clone, Debug, Default)]
#[node(id = "zenfilters.exposure", group = Tone, role = Filter)]
#[node(label = "Exposure")]
#[node(format(preferred = OklabF32, alpha = Skip))]
#[node(coalesce = "fused_adjust")]
pub struct Exposure {
/// Exposure compensation in stops (+/-)
#[param(range(-5.0..=5.0), default = 0.0, identity = 0.0, step = 0.1)]
#[param(unit = "EV", section = "Main", slider = Linear)]
pub stops: f32,
}
This metadata drives:
- Type-safe fluent builders with parameter validation (ranges, required vs optional)
- Enum definitions with labels and descriptions
- RIAPI querystring mapping (every parameter can have multiple kv_keys aliases)
- Conditional visibility rules for UI-aware APIs
- Documentation from descriptions, units, labels, groups
Versioned namespaces
Imageflow.Fluent — existing hand-written V2 API (stays as-is during transition)
Imageflow.Fluent.V2 — generated from OpenAPI schema, replaces hand-written code. Drops deprecated methods.
Imageflow.Fluent.V3 — generated from zennode metadata, adds all new zen operations. Partial classes allow hand-written shims for non-schema nodes.
new ImageJob() gets deprecated in favor of:
ImageJob.V2(backend) with backend selection (Latest, ExactOrFail, Closest)
ImageJob.V3()
Generator tool: Imageflow.CodeGen
A .NET console app in this repo that:
- Loads
imageflow native library via existing P/Invoke bindings
- Calls schema endpoints to extract OpenAPI schema and RIAPI vocabulary from the DLL itself
- For V3: calls zennode registry endpoints (new) to get
NodeSchema metadata for all registered nodes
- Reads a sidecar
node_semantics.json for V2 graph-semantic enrichment (node role classification, C# name mappings, convenience method definitions)
- Emits
.g.cs files committed to the repo (not ephemeral — reviewable in diffs)
Fallback: when native lib isn't available, fetch schema from CI artifacts.
What gets generated
Per-node (both V2 and V3)
BuildNode fluent methods — one per node variant, returning BuildNode for filters, using NodeWithCanvas() for canvas ops
- Parameter validation — V3 gets range validation from
ParamKind metadata
- Convenience overloads — e.g.,
ConstrainWithin(w, h) as shortcut for Constrain(mode=Within)
- XML doc comments from schema descriptions
Shared types
- Enums (Filter, ConstraintMode, CompositingMode, PixelFormat, etc.) with snake_case JSON conversion
- Config DTOs (Constraint, ResampleHints, Watermark, SecurityOptions) with
ToJsonNode() methods
- Tagged unions (Color, RoundCornersMode, EncoderPreset) following serde's externally-tagged pattern
- Encoder preset classes implementing
IEncoderPreset
- Response types for all endpoints (JobResult, ImageInfo, decode/encode results)
- RIAPI types (QueryStringSchema, validation results)
- Schema endpoint wrappers (SchemaClient with typed access to schema/RIAPI/validation)
What stays hand-written
ImageJob (I/O lifecycle, graph assembly, Decode/CreateCanvas/BuildCommandString)
BuildNode class core (To(), NodeWithCanvas(), Branch(), Encode())
BuildItemBase, BuildEndpoint, FinishJobBuilder
- All I/O abstractions (MemorySource, StreamSource, BytesDestination, etc.)
- P/Invoke bindings (NativeMethods, JobContext)
SchemaClient wrapper class (uses generated types, manages JobContext lifecycle)
Generated output structure
src/Imageflow/Generated/
V2/
Enums.g.cs
ConfigTypes.g.cs
ColorTypes.g.cs
SecurityTypes.g.cs
EncoderPresets.g.cs
BuildNode.Operations.g.cs
BuildNode.Convenience.g.cs
ResponseTypes.g.cs
V3/
Enums.g.cs
Nodes.g.cs -- all zen node fluent methods
NodeParams.g.cs -- parameter DTOs with validation
EncoderPresets.g.cs
BuildNode.Operations.g.cs
ResponseTypes.g.cs
Shared/
SchemaTypes.g.cs -- RIAPI schema types
RiapiTypes.g.cs -- RIAPI vocabulary
Constraints
- AOT/trimming compatible: all serialization via
JsonObject/JsonNode (no reflection). Response deserialization via [JsonSerializable] context. No Newtonsoft.Json.
- Generated code is committed: appears in diffs, reviewable. Not ephemeral build artifacts.
- Backward compatible: V2 generated code must produce identical JSON to the current hand-written code (verified by JSON equivalence tests).
- Package validation:
EnablePackageValidation catches binary API breaks.
Schema endpoints already available in native lib
| Endpoint |
Returns |
v1/schema/openapi/latest/get |
Full OpenAPI 3.1 schema as JSON string |
v1/schema/riapi/latest/get |
RIAPI querystring schema |
v1/schema/riapi/latest/list_keys |
List of 100+ supported RIAPI keys |
v1/schema/riapi/latest/validate |
Validate a querystring against schema |
v1/schema/list-schema-endpoints |
List all schema endpoints |
v1/schema/json/latest/v1/all |
JSON schemas for all V1 endpoints (with json-schema feature) |
Why not alternatives
- Roslyn Source Generator: can't read external 112KB JSON or call native P/Invoke at compile time. Opaque output, hard to diff.
- T4 Templates: deprecated, poor tooling, no AOT awareness.
- OpenAPI Generator CLI: produces
NodeOneOf10ExpandCanvas-style naming, uses Newtonsoft.Json, no graph-semantic awareness. Fine for other language bindings (TypeScript/Ruby), wrong tool for the fluent API.
- Hand-written V3: too many nodes (zenfilters alone has 30+), each with rich typed parameters. The metadata is the code — generating from it is the only maintainable path.
Summary
The hand-written fluent API (~2000 LOC of BuildNode methods, encoder presets, enum converters, DTO serialization) mirrors types defined in the native imageflow Rust source. As Imageflow 3 drastically expands the API surface, maintaining hand-written parity is untenable. We need schema-driven code generation for versioned namespaces (
Imageflow.Fluent.V2,Imageflow.Fluent.V3).Architecture
Two schema sources
V2 (current API, OpenAPI): The native library embeds an OpenAPI 3.1 schema (94 component schemas) accessible via
imageflow_context_send_json("v1/schema/openapi/latest/get", "{}"). TheNodeenum is aoneOfwith serde-style tagged variants.EncoderPresetis similar. All types deriveSerialize/Deserialize+ utoipaToSchema.V3 (new API, zennode): The zennode metadata system provides much richer per-node information than OpenAPI alone:
NodeSchema: id, label, description, group, role, params, tags, version, compat_version, json_key, coalesce info, format preferencesParamDesc: name, label, description, kind (11 types: Float/Int/U32/Bool/Str/Enum/FloatArray/Color/Json/Object/TaggedUnion), unit, section, slider mapping, kv_keys (RIAPI aliases), since_version, visible_when, optional, json_name, json_aliasesParamKind: carries min/max/default/identity/step for numerics, variants for enums, sub-params for objects, tagged variants for unionsNodeRole: Decode, Geometry, Orient, Resize, Filter, Composite, Analysis, Encode, Meta, CanvasNodeGroup: Decode, Encode, Tone, ToneRange, ToneMap, Color, Detail, Effects, Geometry, Layout, Canvas, Composite, Quantize, Analysis, Hdr, Raw, Auto, OtherEach zen crate (zenfilters, zenresize, etc.) defines nodes via
#[derive(Node)]with rich annotations:This metadata drives:
Versioned namespaces
Imageflow.Fluent— existing hand-written V2 API (stays as-is during transition)Imageflow.Fluent.V2— generated from OpenAPI schema, replaces hand-written code. Drops deprecated methods.Imageflow.Fluent.V3— generated from zennode metadata, adds all new zen operations. Partial classes allow hand-written shims for non-schema nodes.new ImageJob()gets deprecated in favor of:ImageJob.V2(backend)with backend selection (Latest, ExactOrFail, Closest)ImageJob.V3()Generator tool:
Imageflow.CodeGenA .NET console app in this repo that:
imageflownative library via existing P/Invoke bindingsNodeSchemametadata for all registered nodesnode_semantics.jsonfor V2 graph-semantic enrichment (node role classification, C# name mappings, convenience method definitions).g.csfiles committed to the repo (not ephemeral — reviewable in diffs)Fallback: when native lib isn't available, fetch schema from CI artifacts.
What gets generated
Per-node (both V2 and V3)
BuildNodefluent methods — one per node variant, returningBuildNodefor filters, usingNodeWithCanvas()for canvas opsParamKindmetadataConstrainWithin(w, h)as shortcut forConstrain(mode=Within)Shared types
ToJsonNode()methodsIEncoderPresetWhat stays hand-written
ImageJob(I/O lifecycle, graph assembly, Decode/CreateCanvas/BuildCommandString)BuildNodeclass core (To(),NodeWithCanvas(),Branch(),Encode())BuildItemBase,BuildEndpoint,FinishJobBuilderSchemaClientwrapper class (uses generated types, manages JobContext lifecycle)Generated output structure
Constraints
JsonObject/JsonNode(no reflection). Response deserialization via[JsonSerializable]context. No Newtonsoft.Json.EnablePackageValidationcatches binary API breaks.Schema endpoints already available in native lib
v1/schema/openapi/latest/getv1/schema/riapi/latest/getv1/schema/riapi/latest/list_keysv1/schema/riapi/latest/validatev1/schema/list-schema-endpointsv1/schema/json/latest/v1/alljson-schemafeature)Why not alternatives
NodeOneOf10ExpandCanvas-style naming, uses Newtonsoft.Json, no graph-semantic awareness. Fine for other language bindings (TypeScript/Ruby), wrong tool for the fluent API.