Skip to main content

lenso_contract_codegen/
lib.rs

1//! Portable Capability Descriptor validation and binding generation.
2//!
3//! The Descriptor and its package-local JSON Schemas are the only portable
4//! contract source. This crate deliberately lives above the Kernel: it reads
5//! authoring files, checks evolution rules, and emits deterministic Rust and
6//! TypeScript artifacts before an App is booted.
7
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    error::Error,
11    fmt,
12    fmt::Write as _,
13    fs,
14    path::{Path, PathBuf},
15};
16
17use serde_json::{Map, Value};
18
19mod browser;
20mod ir;
21mod rust_debug;
22mod wit;
23
24pub use browser::generate_browser_request_client;
25
26use ir::{ContractIr, ErrorVariantIr, FieldIr, ObjectAdditionalIr, OperationIr, TypeIr};
27
28const GENERATED_HEADER: &str = "// @generated by lenso-contract-codegen; do not edit.\n";
29const TYPESCRIPT_HEADER: &str = "/* @generated by lenso-contract-codegen; do not edit. */\n";
30const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
31
32/// A parsed Descriptor `SemVer`.
33#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
34struct Version(semver::Version);
35
36impl Version {
37    fn parse(value: &str) -> Result<Self, CodegenError> {
38        semver::Version::parse(value)
39            .map(Self)
40            .map_err(|error| CodegenError::InvalidDescriptor {
41                detail: format!("Descriptor version `{value}` is not valid SemVer: {error}"),
42            })
43    }
44
45    const fn major(&self) -> u64 {
46        self.0.major
47    }
48
49    const fn minor(&self) -> u64 {
50        self.0.minor
51    }
52}
53
54fn parse_numeric_component(value: &str) -> Option<u64> {
55    (!value.is_empty() && (value == "0" || !value.starts_with('0')))
56        .then(|| value.parse().ok())
57        .flatten()
58}
59
60#[derive(Clone, Debug)]
61struct Operation {
62    name: String,
63    interaction: String,
64    request_schema: Value,
65    response_schema: Value,
66    domain_error_schema: Value,
67}
68
69fn contract_ir(descriptor: &Descriptor) -> ContractIr {
70    ContractIr {
71        capability_id: descriptor.capability_id.clone(),
72        version: descriptor.version.clone(),
73        portable: descriptor.portable,
74        cross_lane_transfer: descriptor.cross_lane_transfer,
75        operations: descriptor
76            .operations
77            .iter()
78            .map(|operation| OperationIr {
79                name: operation.name.clone(),
80                interaction: operation.interaction.clone(),
81                request: type_ir_from_schema(&operation.request_schema),
82                response: type_ir_from_schema(&operation.response_schema),
83                domain_errors: error_variant_ir_definitions(&operation.domain_error_schema),
84            })
85            .collect(),
86    }
87}
88
89fn type_ir_from_schema(schema: &Value) -> TypeIr {
90    if let Some(types) = schema.get("type").and_then(Value::as_array) {
91        let non_null_types = types
92            .iter()
93            .filter(|schema_type| *schema_type != "null")
94            .cloned()
95            .collect::<Vec<_>>();
96        if non_null_types.is_empty() {
97            return TypeIr::Null;
98        }
99        if non_null_types.len() == 1 {
100            let mut narrowed = schema.as_object().cloned().unwrap_or_default();
101            narrowed.insert("type".to_owned(), non_null_types[0].clone());
102            let base = type_ir_non_null(&Value::Object(narrowed));
103            return if non_null_types.len() == types.len() {
104                base
105            } else {
106                TypeIr::Nullable(Box::new(base))
107            };
108        }
109    }
110    type_ir_non_null(schema)
111}
112
113fn type_ir_non_null(schema: &Value) -> TypeIr {
114    let Some(object) = schema.as_object() else {
115        return TypeIr::Any;
116    };
117    if let Some(values) = object.get("enum").and_then(Value::as_array) {
118        return TypeIr::Enum(
119            values
120                .iter()
121                .filter_map(Value::as_str)
122                .map(ToOwned::to_owned)
123                .collect(),
124        );
125    }
126    if let Some(schema_type) = object.get("type").and_then(Value::as_str) {
127        return match schema_type {
128            "object" => {
129                let required = required_fields(schema);
130                let fields = object
131                    .get("properties")
132                    .and_then(Value::as_object)
133                    .into_iter()
134                    .flatten()
135                    .map(|(name, schema)| FieldIr {
136                        name: name.clone(),
137                        required: required.contains(name),
138                        sensitive: schema
139                            .get("x-lenso-sensitive")
140                            .and_then(Value::as_bool)
141                            .unwrap_or(false),
142                        ty: type_ir_from_schema(schema),
143                    })
144                    .collect();
145                let additional = match object.get("additionalProperties") {
146                    Some(Value::Bool(false)) => ObjectAdditionalIr::Closed,
147                    Some(Value::Bool(true)) | None => ObjectAdditionalIr::Any,
148                    Some(schema) => {
149                        ObjectAdditionalIr::Typed(Box::new(type_ir_from_schema(schema)))
150                    }
151                };
152                TypeIr::Object { fields, additional }
153            }
154            "array" => object.get("items").map_or(TypeIr::Any, |items| {
155                TypeIr::Array(Box::new(type_ir_from_schema(items)))
156            }),
157            "string" => match object.get("format").and_then(Value::as_str) {
158                Some("int64") => TypeIr::Int64,
159                Some("uint64") => TypeIr::Uint64,
160                Some("byte") => TypeIr::Bytes,
161                Some("date-time") => TypeIr::Timestamp,
162                Some("duration") => TypeIr::Duration,
163                _ => TypeIr::String,
164            },
165            "integer" => TypeIr::Integer,
166            "number" => TypeIr::Number,
167            "boolean" => TypeIr::Boolean,
168            "null" => TypeIr::Null,
169            _ => TypeIr::Any,
170        };
171    }
172    TypeIr::Any
173}
174
175/// A validated, self-contained portable Capability Descriptor.
176#[derive(Clone, Debug)]
177pub struct Descriptor {
178    capability_id: String,
179    capability_major: u64,
180    version: String,
181    parsed_version: Version,
182    portable: bool,
183    cross_lane_transfer: bool,
184    operations: Vec<Operation>,
185}
186
187impl Descriptor {
188    /// Returns the stable Capability series identity, including its major.
189    #[must_use]
190    pub fn capability_id(&self) -> &str {
191        &self.capability_id
192    }
193
194    /// Returns the major encoded in the Capability identity.
195    #[must_use]
196    pub const fn capability_major(&self) -> u64 {
197        self.capability_major
198    }
199
200    /// Returns the exact Descriptor `SemVer` selected for generation.
201    #[must_use]
202    pub fn version(&self) -> &str {
203        &self.version
204    }
205
206    /// Returns whether the Capability is intended to cross Runtime Adapters.
207    #[must_use]
208    pub const fn portable(&self) -> bool {
209        self.portable
210    }
211
212    /// Returns whether generated native values support transfer across Execution Lanes.
213    #[must_use]
214    pub const fn cross_lane_transfer(&self) -> bool {
215        self.cross_lane_transfer
216    }
217
218    /// Returns the stable Operation names in deterministic lexical order.
219    #[must_use]
220    pub fn operation_names(&self) -> Vec<&str> {
221        self.operations
222            .iter()
223            .map(|operation| operation.name.as_str())
224            .collect()
225    }
226}
227
228/// Metadata embedded in every generated language artifact.
229#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct GeneratedMetadata {
231    /// Stable `namespace.name@major` identity.
232    pub capability_id: String,
233    /// Exact Descriptor `SemVer`.
234    pub descriptor_version: String,
235    /// Whether the source Descriptor is portable.
236    pub portable: bool,
237    /// Whether generated native values may transfer across Execution Lanes.
238    pub cross_lane_transfer: bool,
239}
240
241/// One language projection emitted from a portable Capability contract.
242#[derive(Clone, Copy, Debug, Eq, PartialEq)]
243pub enum ProjectionLanguage {
244    /// Native Rust value, consumer, and provider bindings.
245    Rust,
246    /// TypeScript value, consumer, and provider bindings.
247    TypeScript,
248    /// WebAssembly Component Model WIT provider and consumer worlds.
249    Wit,
250    /// Rust bindings plus the generated byte-Adapter Capability codec.
251    RustRuntime,
252}
253
254/// One independently generated language projection.
255#[derive(Clone, Debug, Eq, PartialEq)]
256pub struct GeneratedProjection {
257    /// Metadata embedded in the generated projection.
258    pub metadata: GeneratedMetadata,
259    /// Selected target language.
260    pub language: ProjectionLanguage,
261    /// Generated provider, client, value, and error bindings.
262    pub source: String,
263}
264
265/// Compatibility aggregate for callers that intentionally generate both projections.
266#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct GeneratedArtifacts {
268    /// Metadata shared by all generated languages.
269    pub metadata: GeneratedMetadata,
270    /// Rust provider, client, value, and error bindings.
271    pub rust: String,
272    /// TypeScript provider, client, value, and error bindings.
273    pub typescript: String,
274}
275
276/// Errors produced while parsing, validating, or generating a contract.
277#[derive(Debug)]
278pub enum CodegenError {
279    /// A file could not be read.
280    Io {
281        path: PathBuf,
282        source: std::io::Error,
283    },
284    /// Descriptor or Schema data is malformed.
285    InvalidDescriptor { detail: String },
286    /// A Schema cannot be used by the portable value profile.
287    UnsupportedSchema { path: PathBuf, detail: String },
288    /// A Descriptor interaction is not supported by the generated language bindings.
289    UnsupportedInteraction {
290        operation: String,
291        interaction: String,
292    },
293    /// A portable JSON Schema shape has no exact Component Model representation.
294    UnsupportedWit { detail: String },
295    /// A JSON value would be lossy when represented by JavaScript numbers.
296    InvalidPortableValue { path: String, detail: String },
297    /// A checked-in generated artifact differs from its source.
298    GeneratedArtifactDrift { path: PathBuf },
299}
300
301impl fmt::Display for CodegenError {
302    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
303        match self {
304            Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
305            Self::InvalidDescriptor { detail } => {
306                write!(formatter, "invalid Descriptor: {detail}")
307            }
308            Self::UnsupportedSchema { path, detail } => {
309                write!(formatter, "unsupported Schema {}: {detail}", path.display())
310            }
311            Self::UnsupportedInteraction {
312                operation,
313                interaction,
314            } => write!(
315                formatter,
316                "Operation `{operation}` uses unsupported interaction `{interaction}`"
317            ),
318            Self::UnsupportedWit { detail } => {
319                write!(formatter, "unsupported WIT projection: {detail}")
320            }
321            Self::InvalidPortableValue { path, detail } => {
322                write!(formatter, "invalid portable value at `{path}`: {detail}")
323            }
324            Self::GeneratedArtifactDrift { path } => {
325                write!(formatter, "generated artifact is stale: {}", path.display())
326            }
327        }
328    }
329}
330
331impl Error for CodegenError {
332    fn source(&self) -> Option<&(dyn Error + 'static)> {
333        match self {
334            Self::Io { source, .. } => Some(source),
335            _ => None,
336        }
337    }
338}
339
340/// Reasons why a Descriptor evolution is not safe for an existing Capability
341/// series.
342#[derive(Clone, Debug, Eq, PartialEq)]
343pub enum CompatibilityError {
344    /// The two files do not describe the same Capability series.
345    IdentityChanged { from: String, to: String },
346    /// The new Descriptor is not newer than the old one.
347    VersionNotAdvanced { from: String, to: String },
348    /// The change is not allowed by the old series and requires a new major.
349    BreakingChanges { changes: Vec<String> },
350}
351
352impl fmt::Display for CompatibilityError {
353    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
354        match self {
355            Self::IdentityChanged { from, to } => {
356                write!(
357                    formatter,
358                    "Capability identity changed from `{from}` to `{to}`"
359                )
360            }
361            Self::VersionNotAdvanced { from, to } => {
362                write!(
363                    formatter,
364                    "Descriptor version must advance from `{from}` to `{to}`"
365                )
366            }
367            Self::BreakingChanges { changes } => write!(
368                formatter,
369                "breaking Descriptor changes require a new major: {}",
370                changes.join("; ")
371            ),
372        }
373    }
374}
375
376impl Error for CompatibilityError {}
377
378/// Reads and validates one Descriptor, resolving package-local `$ref` files.
379#[allow(clippy::too_many_lines)]
380pub fn load_descriptor(path: &Path) -> Result<Descriptor, CodegenError> {
381    let descriptor_path = canonical_path(path)?;
382    let package_root = descriptor_path
383        .parent()
384        .unwrap_or_else(|| Path::new("."))
385        .to_path_buf();
386    let descriptor_value = read_json(&descriptor_path)?;
387    let object = descriptor_value
388        .as_object()
389        .ok_or_else(|| CodegenError::InvalidDescriptor {
390            detail: "the Descriptor root must be an object".to_owned(),
391        })?;
392
393    let capability_id = required_string(object, "id")?;
394    let (identity, identity_major) = capability_identity(&capability_id)?;
395    let version = required_string(object, "version")?;
396    let parsed_version = Version::parse(&version)?;
397    let portable = object
398        .get("portable")
399        .and_then(Value::as_bool)
400        .unwrap_or(false);
401    let cross_lane_transfer = object
402        .get("cross_lane_transfer")
403        .and_then(Value::as_bool)
404        .unwrap_or(false);
405    let operation_values = object
406        .get("operations")
407        .and_then(Value::as_array)
408        .ok_or_else(|| CodegenError::InvalidDescriptor {
409            detail: "`operations` must be an array".to_owned(),
410        })?;
411    if operation_values.is_empty() {
412        return Err(CodegenError::InvalidDescriptor {
413            detail: "a Descriptor must declare at least one Operation".to_owned(),
414        });
415    }
416
417    let mut operations = Vec::with_capacity(operation_values.len());
418    let mut operation_names = BTreeSet::new();
419    let mut generated_operation_names = BTreeSet::new();
420    let mut generated_type_names = BTreeSet::new();
421    let mut generated_client_method_names =
422        BTreeSet::from(["new".to_owned(), "from_dependencies".to_owned()]);
423    let capability_name = identity
424        .rsplit('.')
425        .next()
426        .map_or_else(|| "Capability".to_owned(), pascal_case);
427    for operation_value in operation_values {
428        let operation =
429            operation_value
430                .as_object()
431                .ok_or_else(|| CodegenError::InvalidDescriptor {
432                    detail: "each Operation must be an object".to_owned(),
433                })?;
434        let name = required_string(operation, "name")?;
435        if !is_identifier(&name) || !is_rust_member_name(&name) {
436            return Err(CodegenError::InvalidDescriptor {
437                detail: format!(
438                    "Operation name `{name}` must start with a letter or `_` and contain only letters, digits, `_`, or `-`"
439                ),
440            });
441        }
442        if !operation_names.insert(name.clone()) {
443            return Err(CodegenError::InvalidDescriptor {
444                detail: format!("Operation `{name}` is declared more than once"),
445            });
446        }
447        for generated_name in [rust_field_name(&name), pascal_case(&name)] {
448            if !generated_operation_names.insert(generated_name.clone()) {
449                return Err(CodegenError::InvalidDescriptor {
450                    detail: format!(
451                        "Operation name `{name}` collides after code generation as `{generated_name}`"
452                    ),
453                });
454            }
455        }
456        let operation_name = pascal_case(&name);
457        let mut generated_names = vec![
458            format!("{operation_name}Request"),
459            format!("{operation_name}Response"),
460            format!("{operation_name}Error"),
461            format!("{operation_name}InvocationError"),
462            format!("{operation_name}Result"),
463            format!("{capability_name}{operation_name}"),
464        ];
465        generated_names.push(if operation_values.len() == 1 {
466            format!("{capability_name}InvocationError")
467        } else {
468            format!("{capability_name}{operation_name}InvocationError")
469        });
470        for generated_name in generated_names {
471            if !is_generated_type_name(&generated_name)
472                || !generated_type_names.insert(generated_name.clone())
473            {
474                return Err(CodegenError::InvalidDescriptor {
475                    detail: format!(
476                        "Operation name `{name}` collides after type generation as `{generated_name}`"
477                    ),
478                });
479            }
480        }
481        let interaction = required_string(operation, "interaction")?;
482        if !matches!(interaction.as_str(), "request" | "stream" | "event") {
483            return Err(CodegenError::InvalidDescriptor {
484                detail: format!("Operation `{name}` has unsupported interaction `{interaction}`"),
485            });
486        }
487        if matches!(interaction.as_str(), "request" | "stream" | "event") {
488            let client_method_name = rust_field_name(&name);
489            for generated_name in [
490                client_method_name.clone(),
491                format!("{client_method_name}_with_context"),
492            ] {
493                if !generated_client_method_names.insert(generated_name.clone()) {
494                    return Err(CodegenError::InvalidDescriptor {
495                        detail: format!(
496                            "Operation name `{name}` collides with the generated Client API as `{generated_name}`"
497                        ),
498                    });
499                }
500            }
501        }
502        let request_schema_path =
503            schema_path(&descriptor_path, operation, "request_schema", &name)?;
504        let response_schema_path =
505            schema_path(&descriptor_path, operation, "response_schema", &name)?;
506        let domain_error_schema_path =
507            schema_path(&descriptor_path, operation, "domain_error_schema", &name)?;
508        let mut ref_stack = Vec::new();
509        let request_source = read_json(&request_schema_path)?;
510        let request_schema = resolve_refs(
511            &request_source,
512            &request_source,
513            &request_schema_path,
514            &package_root,
515            &mut ref_stack,
516        )?;
517        validate_schema_profile(&request_schema, &request_schema_path)?;
518        validate_value_generation_schema(&request_schema, &request_schema_path)?;
519        let mut ref_stack = Vec::new();
520        let response_source = read_json(&response_schema_path)?;
521        let response_schema = resolve_refs(
522            &response_source,
523            &response_source,
524            &response_schema_path,
525            &package_root,
526            &mut ref_stack,
527        )?;
528        validate_schema_profile(&response_schema, &response_schema_path)?;
529        validate_value_generation_schema(&response_schema, &response_schema_path)?;
530        let mut ref_stack = Vec::new();
531        let domain_error_source = read_json(&domain_error_schema_path)?;
532        let domain_error_schema = resolve_refs(
533            &domain_error_source,
534            &domain_error_source,
535            &domain_error_schema_path,
536            &package_root,
537            &mut ref_stack,
538        )?;
539        validate_schema_profile(&domain_error_schema, &domain_error_schema_path)?;
540        validate_domain_error_schema(&domain_error_schema, &domain_error_schema_path)?;
541        operations.push(Operation {
542            name,
543            interaction,
544            request_schema,
545            response_schema,
546            domain_error_schema,
547        });
548    }
549    operations.sort_by(|left, right| left.name.cmp(&right.name));
550
551    Ok(Descriptor {
552        capability_id: format!("{identity}@{identity_major}"),
553        capability_major: identity_major,
554        version,
555        parsed_version,
556        portable,
557        cross_lane_transfer,
558        operations,
559    })
560}
561
562fn required_string(object: &Map<String, Value>, key: &str) -> Result<String, CodegenError> {
563    object
564        .get(key)
565        .and_then(Value::as_str)
566        .filter(|value| !value.is_empty())
567        .map(ToOwned::to_owned)
568        .ok_or_else(|| CodegenError::InvalidDescriptor {
569            detail: format!("`{key}` must be a non-empty string"),
570        })
571}
572
573fn capability_identity(value: &str) -> Result<(String, u64), CodegenError> {
574    let Some((identity, major)) = value.rsplit_once('@') else {
575        return Err(CodegenError::InvalidDescriptor {
576            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
577        });
578    };
579    let generated_name = identity
580        .rsplit('.')
581        .next()
582        .map(pascal_case)
583        .unwrap_or_default();
584    if identity.is_empty()
585        || identity.split('.').count() < 2
586        || !identity.split('.').all(is_identifier)
587        || !is_generated_type_name(&generated_name)
588    {
589        return Err(CodegenError::InvalidDescriptor {
590            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
591        });
592    }
593    let Some(major) = parse_numeric_component(major) else {
594        return Err(CodegenError::InvalidDescriptor {
595            detail: format!("Capability id `{value}` has an invalid major"),
596        });
597    };
598    Ok((identity.to_owned(), major))
599}
600
601fn schema_path(
602    descriptor_path: &Path,
603    operation: &Map<String, Value>,
604    key: &str,
605    operation_name: &str,
606) -> Result<PathBuf, CodegenError> {
607    let value = required_string(operation, key).map_err(|error| match error {
608        CodegenError::InvalidDescriptor { detail } => CodegenError::InvalidDescriptor {
609            detail: format!("Operation `{operation_name}`: {detail}"),
610        },
611        other => other,
612    })?;
613    let candidate = descriptor_path
614        .parent()
615        .unwrap_or_else(|| Path::new("."))
616        .join(value);
617    let canonical = canonical_path(&candidate)?;
618    let package_root = canonical_path(descriptor_path.parent().unwrap_or_else(|| Path::new(".")))?;
619    if !canonical.starts_with(&package_root) {
620        return Err(CodegenError::UnsupportedSchema {
621            path: canonical,
622            detail: "Schema paths must remain inside the Descriptor package".to_owned(),
623        });
624    }
625    Ok(canonical)
626}
627
628fn canonical_path(path: &Path) -> Result<PathBuf, CodegenError> {
629    fs::canonicalize(path).map_err(|source| CodegenError::Io {
630        path: path.to_path_buf(),
631        source,
632    })
633}
634
635fn read_json(path: &Path) -> Result<Value, CodegenError> {
636    let source = fs::read_to_string(path).map_err(|source| CodegenError::Io {
637        path: path.to_path_buf(),
638        source,
639    })?;
640    serde_json::from_str(&source).map_err(|error| CodegenError::InvalidDescriptor {
641        detail: format!("{}: {error}", path.display()),
642    })
643}
644
645fn resolve_refs(
646    value: &Value,
647    root: &Value,
648    source_path: &Path,
649    package_root: &Path,
650    stack: &mut Vec<(PathBuf, String)>,
651) -> Result<Value, CodegenError> {
652    match value {
653        Value::Object(object) => {
654            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
655                let target_value = if reference.starts_with('#') {
656                    let fragment = reference.strip_prefix('#').unwrap_or_default();
657                    let target_path = source_path.to_path_buf();
658                    let key = (target_path.clone(), reference.to_owned());
659                    if stack.contains(&key) {
660                        return Err(CodegenError::UnsupportedSchema {
661                            path: source_path.to_path_buf(),
662                            detail: format!("cyclic local Schema `$ref` `{reference}`"),
663                        });
664                    }
665                    let target = json_pointer(root, fragment).ok_or_else(|| {
666                        CodegenError::UnsupportedSchema {
667                            path: source_path.to_path_buf(),
668                            detail: format!(
669                                "local JSON Pointer `$ref` `{reference}` was not found"
670                            ),
671                        }
672                    })?;
673                    stack.push(key);
674                    let resolved = resolve_refs(target, root, source_path, package_root, stack)?;
675                    stack.pop();
676                    resolved
677                } else {
678                    let (reference_path, fragment) =
679                        reference.split_once('#').unwrap_or((reference, ""));
680                    let target_path = source_path
681                        .parent()
682                        .unwrap_or_else(|| Path::new("."))
683                        .join(reference_path);
684                    let target_path = canonical_path(&target_path)?;
685                    if !target_path.starts_with(package_root) {
686                        return Err(CodegenError::UnsupportedSchema {
687                            path: source_path.to_path_buf(),
688                            detail: format!(
689                                "external Schema `$ref` `{reference}` leaves the Descriptor package"
690                            ),
691                        });
692                    }
693                    let key = (target_path.clone(), format!("#{fragment}"));
694                    if stack.contains(&key) {
695                        return Err(CodegenError::UnsupportedSchema {
696                            path: source_path.to_path_buf(),
697                            detail: format!(
698                                "cyclic Schema `$ref` through {}",
699                                target_path.display()
700                            ),
701                        });
702                    }
703                    let target_root = read_json(&target_path)?;
704                    let target = json_pointer(&target_root, fragment).ok_or_else(|| {
705                        CodegenError::UnsupportedSchema {
706                            path: source_path.to_path_buf(),
707                            detail: format!(
708                                "external JSON Pointer `$ref` `{reference}` was not found"
709                            ),
710                        }
711                    })?;
712                    stack.push(key);
713                    let resolved =
714                        resolve_refs(target, &target_root, &target_path, package_root, stack)?;
715                    stack.pop();
716                    resolved
717                };
718                let mut merged = target_value.as_object().cloned().ok_or_else(|| {
719                    CodegenError::UnsupportedSchema {
720                        path: source_path.to_path_buf(),
721                        detail: "a `$ref` target must resolve to an object Schema".to_owned(),
722                    }
723                })?;
724                for (key, child) in object {
725                    if key != "$ref" {
726                        merged.insert(
727                            key.clone(),
728                            resolve_refs(child, root, source_path, package_root, stack)?,
729                        );
730                    }
731                }
732                return Ok(Value::Object(merged));
733            }
734            let mut resolved = Map::new();
735            for (key, child) in object {
736                resolved.insert(
737                    key.clone(),
738                    resolve_refs(child, root, source_path, package_root, stack)?,
739                );
740            }
741            Ok(Value::Object(resolved))
742        }
743        Value::Array(values) => values
744            .iter()
745            .map(|child| resolve_refs(child, root, source_path, package_root, stack))
746            .collect::<Result<Vec<_>, _>>()
747            .map(Value::Array),
748        _ => Ok(value.clone()),
749    }
750}
751
752fn json_pointer<'a>(root: &'a Value, fragment: &str) -> Option<&'a Value> {
753    if fragment.is_empty() {
754        return Some(root);
755    }
756    let pointer = fragment.strip_prefix('/')?;
757    let mut current = root;
758    for token in pointer.split('/') {
759        let token = token.replace("~1", "/").replace("~0", "~");
760        current = match current {
761            Value::Object(object) => object.get(&token)?,
762            Value::Array(array) => array.get(token.parse::<usize>().ok()?)?,
763            _ => return None,
764        };
765    }
766    Some(current)
767}
768
769fn validate_schema_profile(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
770    let Some(object) = schema.as_object() else {
771        return Err(CodegenError::UnsupportedSchema {
772            path: source_path.to_path_buf(),
773            detail: "a JSON Schema must be an object".to_owned(),
774        });
775    };
776
777    if let Some(format) = object.get("format").and_then(Value::as_str) {
778        if !matches!(
779            format,
780            "int64" | "uint64" | "byte" | "date-time" | "duration"
781        ) {
782            return Err(CodegenError::UnsupportedSchema {
783                path: source_path.to_path_buf(),
784                detail: format!("format `{format}` is outside the portable value profile"),
785            });
786        }
787        if !schema_includes_type(object, "string") {
788            return Err(CodegenError::UnsupportedSchema {
789                path: source_path.to_path_buf(),
790                detail: format!("portable format `{format}` must be attached to a string Schema"),
791            });
792        }
793    }
794
795    if let Some(schema_type) = object.get("type") {
796        match schema_type {
797            Value::String(schema_type) => validate_schema_type(schema_type, object, source_path)?,
798            Value::Array(types) => {
799                if types.is_empty() {
800                    return Err(CodegenError::UnsupportedSchema {
801                        path: source_path.to_path_buf(),
802                        detail: "a Schema type union cannot be empty".to_owned(),
803                    });
804                }
805                for schema_type in types {
806                    let Some(schema_type) = schema_type.as_str() else {
807                        return Err(CodegenError::UnsupportedSchema {
808                            path: source_path.to_path_buf(),
809                            detail: "Schema type unions must contain strings".to_owned(),
810                        });
811                    };
812                    validate_schema_type(schema_type, object, source_path)?;
813                }
814            }
815            _ => {
816                return Err(CodegenError::UnsupportedSchema {
817                    path: source_path.to_path_buf(),
818                    detail: "Schema `type` must be a string or array".to_owned(),
819                });
820            }
821        }
822    } else if let Some(alternatives) = object
823        .get("oneOf")
824        .or_else(|| object.get("anyOf"))
825        .and_then(Value::as_array)
826    {
827        for alternative in alternatives {
828            validate_schema_profile(alternative, source_path)?;
829        }
830    } else if !object.contains_key("const") && !object.contains_key("enum") {
831        return Err(CodegenError::UnsupportedSchema {
832            path: source_path.to_path_buf(),
833            detail: "Schema needs a supported `type`, `oneOf`, `anyOf`, `const`, or `enum`"
834                .to_owned(),
835        });
836    }
837
838    if let Some(properties) = object.get("properties") {
839        let Some(properties) = properties.as_object() else {
840            return Err(CodegenError::UnsupportedSchema {
841                path: source_path.to_path_buf(),
842                detail: "Schema `properties` must be an object".to_owned(),
843            });
844        };
845        for property in properties.values() {
846            validate_schema_profile(property, source_path)?;
847        }
848    }
849    if let Some(items) = object.get("items") {
850        validate_schema_profile(items, source_path)?;
851    }
852    if let Some(additional) = object.get("additionalProperties")
853        && !additional.is_boolean()
854    {
855        validate_schema_profile(additional, source_path)?;
856    }
857    if let Some(definitions) = object.get("$defs").and_then(Value::as_object) {
858        for definition in definitions.values() {
859            validate_schema_profile(definition, source_path)?;
860        }
861    }
862    if let Some(alternatives) = object
863        .get("oneOf")
864        .or_else(|| object.get("anyOf"))
865        .and_then(Value::as_array)
866    {
867        for alternative in alternatives {
868            validate_schema_profile(alternative, source_path)?;
869        }
870    }
871    Ok(())
872}
873
874fn validate_schema_type(
875    schema_type: &str,
876    schema: &Map<String, Value>,
877    source_path: &Path,
878) -> Result<(), CodegenError> {
879    if !matches!(
880        schema_type,
881        "object" | "array" | "string" | "integer" | "number" | "boolean" | "null"
882    ) {
883        return Err(CodegenError::UnsupportedSchema {
884            path: source_path.to_path_buf(),
885            detail: format!("Schema type `{schema_type}` is outside the portable profile"),
886        });
887    }
888    if schema_type == "array" && !schema.contains_key("items") {
889        return Err(CodegenError::UnsupportedSchema {
890            path: source_path.to_path_buf(),
891            detail: "array Schemas must declare `items`".to_owned(),
892        });
893    }
894    Ok(())
895}
896
897fn schema_includes_type(schema: &Map<String, Value>, expected: &str) -> bool {
898    match schema.get("type") {
899        Some(Value::String(schema_type)) => schema_type == expected,
900        Some(Value::Array(types)) => types.iter().any(|schema_type| {
901            schema_type
902                .as_str()
903                .is_some_and(|schema_type| schema_type == expected)
904        }),
905        _ => false,
906    }
907}
908
909#[allow(clippy::too_many_lines)]
910fn validate_value_generation_schema(
911    schema: &Value,
912    source_path: &Path,
913) -> Result<(), CodegenError> {
914    let Some(object) = schema.as_object() else {
915        return Err(CodegenError::UnsupportedSchema {
916            path: source_path.to_path_buf(),
917            detail: "generated value Schemas must be objects".to_owned(),
918        });
919    };
920    if object.contains_key("oneOf") || object.contains_key("anyOf") {
921        return Err(CodegenError::UnsupportedSchema {
922            path: source_path.to_path_buf(),
923            detail: "generated value Schemas do not support oneOf/anyOf unions".to_owned(),
924        });
925    }
926    if object.contains_key("allOf") {
927        return Err(CodegenError::UnsupportedSchema {
928            path: source_path.to_path_buf(),
929            detail: "generated value Schemas do not support allOf unions".to_owned(),
930        });
931    }
932    if object.contains_key("const") {
933        return Err(CodegenError::UnsupportedSchema {
934            path: source_path.to_path_buf(),
935            detail: "generated value Schemas do not support const fields".to_owned(),
936        });
937    }
938    if let Some(types) = object.get("type").and_then(Value::as_array) {
939        let non_null = types
940            .iter()
941            .filter(|schema_type| *schema_type != "null")
942            .count();
943        if non_null > 1 {
944            return Err(CodegenError::UnsupportedSchema {
945                path: source_path.to_path_buf(),
946                detail: "generated value Schemas only support nullable type unions".to_owned(),
947            });
948        }
949        let mut narrowed = object.clone();
950        let Some(schema_type) = types.iter().find(|schema_type| *schema_type != "null") else {
951            return Ok(());
952        };
953        narrowed.insert("type".to_owned(), schema_type.clone());
954        return validate_value_generation_schema(&Value::Object(narrowed), source_path);
955    }
956    if let Some(values) = object.get("enum").and_then(Value::as_array) {
957        if values.is_empty() || !values.iter().all(Value::is_string) {
958            return Err(CodegenError::UnsupportedSchema {
959                path: source_path.to_path_buf(),
960                detail: "generated enum values must be non-empty strings".to_owned(),
961            });
962        }
963        let mut enum_names = BTreeSet::new();
964        for value in values.iter().filter_map(Value::as_str) {
965            let name = pascal_case(value);
966            if !is_generated_enum_variant(&name) || !enum_names.insert(name) {
967                return Err(CodegenError::UnsupportedSchema {
968                    path: source_path.to_path_buf(),
969                    detail: "generated enum values collide after Rust variant generation"
970                        .to_owned(),
971                });
972            }
973        }
974    }
975    let Some(schema_type) = object.get("type").and_then(Value::as_str) else {
976        if object.contains_key("enum") {
977            return Ok(());
978        }
979        return Err(CodegenError::UnsupportedSchema {
980            path: source_path.to_path_buf(),
981            detail: "generated value Schemas need a supported type or string enum".to_owned(),
982        });
983    };
984    match schema_type {
985        "object" => {
986            let properties = object
987                .get("properties")
988                .and_then(Value::as_object)
989                .cloned()
990                .unwrap_or_default();
991            let mut rust_names = BTreeSet::new();
992            let mut nested_type_names = BTreeSet::new();
993            for (name, property) in &properties {
994                if !rust_names.insert(rust_field_name(name)) {
995                    return Err(CodegenError::UnsupportedSchema {
996                        path: source_path.to_path_buf(),
997                        detail: format!(
998                            "object properties collide after Rust field normalization near `{name}`"
999                        ),
1000                    });
1001                }
1002                if !nested_type_names.insert(pascal_case(name)) {
1003                    return Err(CodegenError::UnsupportedSchema {
1004                        path: source_path.to_path_buf(),
1005                        detail: format!(
1006                            "object properties collide after nested type-name generation near `{name}`"
1007                        ),
1008                    });
1009                }
1010                validate_value_generation_schema(property, source_path)?;
1011            }
1012            match object.get("additionalProperties") {
1013                Some(Value::Bool(false)) => {}
1014                Some(Value::Bool(true)) | None if !properties.is_empty() => {
1015                    return Err(CodegenError::UnsupportedSchema {
1016                        path: source_path.to_path_buf(),
1017                        detail: "open objects with declared properties would lose unknown fields"
1018                            .to_owned(),
1019                    });
1020                }
1021                Some(additional) if !additional.is_boolean() && !properties.is_empty() => {
1022                    return Err(CodegenError::UnsupportedSchema {
1023                        path: source_path.to_path_buf(),
1024                        detail: "objects cannot combine declared properties with a map payload"
1025                            .to_owned(),
1026                    });
1027                }
1028                Some(additional) if !additional.is_boolean() => {
1029                    validate_value_generation_schema(additional, source_path)?;
1030                }
1031                _ => {}
1032            }
1033        }
1034        "array" => {
1035            let items = object
1036                .get("items")
1037                .ok_or_else(|| CodegenError::UnsupportedSchema {
1038                    path: source_path.to_path_buf(),
1039                    detail: "generated array Schemas must declare items".to_owned(),
1040                })?;
1041            validate_value_generation_schema(items, source_path)?;
1042        }
1043        "string" | "integer" | "number" | "boolean" | "null" => {}
1044        _ => {
1045            return Err(CodegenError::UnsupportedSchema {
1046                path: source_path.to_path_buf(),
1047                detail: format!("Schema type `{schema_type}` cannot be generated"),
1048            });
1049        }
1050    }
1051    Ok(())
1052}
1053
1054fn validate_domain_error_schema(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1055    if schema
1056        .as_object()
1057        .is_some_and(|object| object.contains_key("allOf"))
1058    {
1059        return Err(CodegenError::UnsupportedSchema {
1060            path: source_path.to_path_buf(),
1061            detail: "Domain Error Schemas do not support allOf unions".to_owned(),
1062        });
1063    }
1064    let variants = schema
1065        .get("oneOf")
1066        .and_then(Value::as_array)
1067        .filter(|variants| !variants.is_empty())
1068        .ok_or_else(|| CodegenError::UnsupportedSchema {
1069            path: source_path.to_path_buf(),
1070            detail: "Domain Error Schemas must be a non-empty oneOf union".to_owned(),
1071        })?;
1072    let mut codes = BTreeSet::new();
1073    let mut names = BTreeSet::from(["Unknown".to_owned()]);
1074    for variant in variants {
1075        if variant
1076            .as_object()
1077            .is_some_and(|object| object.contains_key("allOf"))
1078        {
1079            return Err(CodegenError::UnsupportedSchema {
1080                path: source_path.to_path_buf(),
1081                detail: "Domain Error variants do not support allOf unions".to_owned(),
1082            });
1083        }
1084        if let Some(code) = variant.get("const").and_then(Value::as_str) {
1085            let name = pascal_case(code);
1086            if !codes.insert(code.to_owned())
1087                || !names.insert(name.clone())
1088                || !is_generated_enum_variant(&name)
1089            {
1090                return Err(CodegenError::UnsupportedSchema {
1091                    path: source_path.to_path_buf(),
1092                    detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1093                });
1094            }
1095            continue;
1096        }
1097        let object = variant
1098            .as_object()
1099            .ok_or_else(|| CodegenError::UnsupportedSchema {
1100                path: source_path.to_path_buf(),
1101                detail: "Domain Error variants must be string consts or objects".to_owned(),
1102            })?;
1103        let properties = object
1104            .get("properties")
1105            .and_then(Value::as_object)
1106            .ok_or_else(|| CodegenError::UnsupportedSchema {
1107                path: source_path.to_path_buf(),
1108                detail: "structured Domain Error variants need properties".to_owned(),
1109            })?;
1110        if object.get("additionalProperties") != Some(&Value::Bool(false))
1111            || !required_fields(variant).contains("code")
1112            || properties
1113                .keys()
1114                .any(|name| name != "code" && name != "payload")
1115        {
1116            return Err(CodegenError::UnsupportedSchema {
1117                path: source_path.to_path_buf(),
1118                detail:
1119                    "structured Domain Error objects must explicitly allow only code and payload"
1120                        .to_owned(),
1121            });
1122        }
1123        let code = object
1124            .get("properties")
1125            .and_then(Value::as_object)
1126            .and_then(|properties| properties.get("code"))
1127            .and_then(|code| code.get("const"))
1128            .and_then(Value::as_str)
1129            .ok_or_else(|| CodegenError::UnsupportedSchema {
1130                path: source_path.to_path_buf(),
1131                detail: "structured Domain Error variants need a const string code".to_owned(),
1132            })?;
1133        let name = pascal_case(code);
1134        if !codes.insert(code.to_owned())
1135            || !names.insert(name.clone())
1136            || !is_generated_enum_variant(&name)
1137        {
1138            return Err(CodegenError::UnsupportedSchema {
1139                path: source_path.to_path_buf(),
1140                detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1141            });
1142        }
1143        if let Some(payload) = object
1144            .get("properties")
1145            .and_then(Value::as_object)
1146            .and_then(|properties| properties.get("payload"))
1147        {
1148            validate_value_generation_schema(payload, source_path)?;
1149        }
1150    }
1151    Ok(())
1152}
1153
1154fn generation_input(path: &Path) -> Result<(GeneratedMetadata, ContractIr), CodegenError> {
1155    let descriptor = load_descriptor(path)?;
1156    let contract = contract_ir(&descriptor);
1157    let metadata = GeneratedMetadata {
1158        capability_id: descriptor.capability_id.clone(),
1159        descriptor_version: descriptor.version.clone(),
1160        portable: descriptor.portable,
1161        cross_lane_transfer: descriptor.cross_lane_transfer,
1162    };
1163    Ok((metadata, contract))
1164}
1165
1166/// Generates one selected language projection from a Descriptor source.
1167pub fn generate_projection(
1168    path: &Path,
1169    language: ProjectionLanguage,
1170) -> Result<GeneratedProjection, CodegenError> {
1171    let (metadata, contract) = generation_input(path)?;
1172    let source = match language {
1173        ProjectionLanguage::Rust => generate_rust(&contract),
1174        ProjectionLanguage::TypeScript => generate_typescript(&contract),
1175        ProjectionLanguage::Wit => wit::generate_wit(&contract)?,
1176        ProjectionLanguage::RustRuntime => generate_rust_runtime(&contract)?,
1177    };
1178    Ok(GeneratedProjection {
1179        metadata,
1180        language,
1181        source,
1182    })
1183}
1184
1185/// Generates both language artifacts from one Descriptor source.
1186pub fn generate(path: &Path) -> Result<GeneratedArtifacts, CodegenError> {
1187    let (metadata, contract) = generation_input(path)?;
1188    Ok(GeneratedArtifacts {
1189        metadata,
1190        rust: generate_rust(&contract),
1191        typescript: generate_typescript(&contract),
1192    })
1193}
1194
1195/// Writes generated artifacts to checked-in paths.
1196pub fn write_generated(
1197    descriptor_path: &Path,
1198    rust_path: &Path,
1199    typescript_path: &Path,
1200) -> Result<(), CodegenError> {
1201    let artifacts = generate(descriptor_path)?;
1202    write_artifact(rust_path, &artifacts.rust)?;
1203    write_artifact(typescript_path, &artifacts.typescript)?;
1204    Ok(())
1205}
1206
1207/// Writes one selected language projection to a checked-in path.
1208pub fn write_projection(
1209    descriptor_path: &Path,
1210    language: ProjectionLanguage,
1211    output_path: &Path,
1212) -> Result<(), CodegenError> {
1213    let projection = generate_projection(descriptor_path, language)?;
1214    write_artifact(output_path, &projection.source)
1215}
1216
1217/// Fails when either checked-in artifact is not exactly reproducible.
1218pub fn check_generated(
1219    descriptor_path: &Path,
1220    rust_path: &Path,
1221    typescript_path: &Path,
1222) -> Result<(), CodegenError> {
1223    let artifacts = generate(descriptor_path)?;
1224    check_artifact(rust_path, &artifacts.rust)?;
1225    check_artifact(typescript_path, &artifacts.typescript)?;
1226    Ok(())
1227}
1228
1229/// Fails when one selected language projection is not exactly reproducible.
1230pub fn check_projection(
1231    descriptor_path: &Path,
1232    language: ProjectionLanguage,
1233    output_path: &Path,
1234) -> Result<(), CodegenError> {
1235    let projection = generate_projection(descriptor_path, language)?;
1236    check_artifact(output_path, &projection.source)
1237}
1238
1239/// Round-trips one JSON value through the portable wire representation.
1240///
1241/// Raw JSON numbers are restricted to JavaScript's safe integer range. Wide
1242/// integers must therefore be represented by the Descriptor's `int64` or
1243/// `uint64` decimal-string formats, which this function preserves exactly.
1244pub fn round_trip_portable_json(value: &Value) -> Result<Value, CodegenError> {
1245    validate_portable_value(value, "$".to_owned())?;
1246    let encoded = serde_json::to_vec(value).expect("JSON values are serializable");
1247    serde_json::from_slice(&encoded).map_err(|error| CodegenError::InvalidPortableValue {
1248        path: "$".to_owned(),
1249        detail: format!("wire JSON could not be decoded: {error}"),
1250    })
1251}
1252
1253/// Validates a wire value against a package-local JSON Schema.
1254///
1255/// This is intentionally a small validator for the Descriptor's portable
1256/// profile rather than a general JSON Schema implementation. It makes the
1257/// profile's loss-sensitive mappings executable: wide integers must use their
1258/// decimal-string formats, bytes must be canonical base64, and timestamp and
1259/// duration strings must have their documented wire shapes.
1260pub fn validate_wire_value(schema_path: &Path, value: &Value) -> Result<(), CodegenError> {
1261    let schema_path = canonical_path(schema_path)?;
1262    let package_root = schema_path
1263        .parent()
1264        .unwrap_or_else(|| Path::new("."))
1265        .to_path_buf();
1266    let source = read_json(&schema_path)?;
1267    let mut ref_stack = Vec::new();
1268    let schema = resolve_refs(
1269        &source,
1270        &source,
1271        &schema_path,
1272        &package_root,
1273        &mut ref_stack,
1274    )?;
1275    validate_schema_profile(&schema, &schema_path)?;
1276    validate_wire_value_inner(&schema, value, "$", &schema_path)
1277}
1278
1279fn validate_wire_value_inner(
1280    schema: &Value,
1281    value: &Value,
1282    path: &str,
1283    source_path: &Path,
1284) -> Result<(), CodegenError> {
1285    if let Some(constant) = schema.get("const")
1286        && value != constant
1287    {
1288        return invalid_wire_value(path, "value does not match the Schema const", source_path);
1289    }
1290    if let Some(values) = schema.get("enum").and_then(Value::as_array)
1291        && !values.iter().any(|candidate| candidate == value)
1292    {
1293        return invalid_wire_value(
1294            path,
1295            "value is not included in the Schema enum",
1296            source_path,
1297        );
1298    }
1299
1300    if let Some(alternatives) = schema.get("oneOf").and_then(Value::as_array) {
1301        let matches = alternatives
1302            .iter()
1303            .filter(|alternative| {
1304                validate_wire_value_inner(alternative, value, path, source_path).is_ok()
1305            })
1306            .count();
1307        if matches == 0
1308            && is_open_domain_error_schema(schema)
1309            && is_unknown_domain_error_wire(schema, value)
1310        {
1311            return validate_portable_value(value, path.to_owned());
1312        }
1313        if matches != 1 {
1314            return invalid_wire_value(
1315                path,
1316                "value must match exactly one Schema oneOf alternative",
1317                source_path,
1318            );
1319        }
1320    }
1321    if let Some(alternatives) = schema.get("anyOf").and_then(Value::as_array)
1322        && !alternatives.iter().any(|alternative| {
1323            validate_wire_value_inner(alternative, value, path, source_path).is_ok()
1324        })
1325    {
1326        return invalid_wire_value(
1327            path,
1328            "value must match at least one Schema anyOf alternative",
1329            source_path,
1330        );
1331    }
1332
1333    match schema.get("type") {
1334        Some(Value::String(schema_type)) => {
1335            validate_wire_type(schema, schema_type, value, path, source_path)
1336        }
1337        Some(Value::Array(types)) => {
1338            let mut errors = Vec::new();
1339            for schema_type in types {
1340                let Some(schema_type) = schema_type.as_str() else {
1341                    continue;
1342                };
1343                match validate_wire_type(schema, schema_type, value, path, source_path) {
1344                    Ok(()) => return Ok(()),
1345                    Err(error) => errors.push(error.to_string()),
1346                }
1347            }
1348            invalid_wire_value(
1349                path,
1350                &format!(
1351                    "value does not match any Schema type ({})",
1352                    errors.join("; ")
1353                ),
1354                source_path,
1355            )
1356        }
1357        Some(_) => invalid_wire_value(path, "Schema type must be a string or array", source_path),
1358        None if schema
1359            .as_object()
1360            .is_some_and(|object| object.contains_key("const") || object.contains_key("enum")) =>
1361        {
1362            Ok(())
1363        }
1364        None => Ok(()),
1365    }
1366}
1367
1368fn is_open_domain_error_schema(schema: &Value) -> bool {
1369    schema
1370        .get("oneOf")
1371        .and_then(Value::as_array)
1372        .is_some_and(|variants| {
1373            !variants.is_empty()
1374                && variants.iter().all(|variant| {
1375                    variant.get("const").and_then(Value::as_str).is_some()
1376                        || variant
1377                            .get("properties")
1378                            .and_then(Value::as_object)
1379                            .and_then(|properties| properties.get("code"))
1380                            .and_then(|code| code.get("const"))
1381                            .and_then(Value::as_str)
1382                            .is_some()
1383                })
1384        })
1385}
1386
1387fn is_unknown_domain_error_wire(schema: &Value, value: &Value) -> bool {
1388    let known_codes = schema
1389        .get("oneOf")
1390        .and_then(Value::as_array)
1391        .into_iter()
1392        .flatten()
1393        .filter_map(|variant| {
1394            variant.get("const").and_then(Value::as_str).or_else(|| {
1395                variant
1396                    .get("properties")
1397                    .and_then(Value::as_object)
1398                    .and_then(|properties| properties.get("code"))
1399                    .and_then(|code| code.get("const"))
1400                    .and_then(Value::as_str)
1401            })
1402        })
1403        .collect::<BTreeSet<_>>();
1404    match value {
1405        Value::String(code) => !known_codes.contains(code.as_str()),
1406        Value::Object(object) => object
1407            .get("code")
1408            .and_then(Value::as_str)
1409            .is_some_and(|code| !known_codes.contains(code)),
1410        _ => false,
1411    }
1412}
1413
1414fn validate_wire_type(
1415    schema: &Value,
1416    schema_type: &str,
1417    value: &Value,
1418    path: &str,
1419    source_path: &Path,
1420) -> Result<(), CodegenError> {
1421    match schema_type {
1422        "object" => validate_wire_object(schema, value, path, source_path),
1423        "array" => validate_wire_array(schema, value, path, source_path),
1424        "string" => validate_wire_string(schema, value, path, source_path),
1425        "integer" => {
1426            if !value.is_number()
1427                || value.as_i64().is_none() && value.as_u64().is_none()
1428                || !is_safe_json_integer(value)
1429            {
1430                return invalid_wire_value(path, "expected an integer", source_path);
1431            }
1432            validate_numeric_constraint(schema, value, path, source_path)
1433        }
1434        "number" => {
1435            if value
1436                .as_number()
1437                .is_none_or(|number| !is_safe_json_number(number))
1438            {
1439                return invalid_wire_value(path, "expected a finite number", source_path);
1440            }
1441            validate_numeric_constraint(schema, value, path, source_path)
1442        }
1443        "boolean" => {
1444            if !value.is_boolean() {
1445                return invalid_wire_value(path, "expected a boolean", source_path);
1446            }
1447            Ok(())
1448        }
1449        "null" => {
1450            if !value.is_null() {
1451                return invalid_wire_value(path, "expected null", source_path);
1452            }
1453            Ok(())
1454        }
1455        _ => invalid_wire_value(
1456            path,
1457            "Schema type is outside the portable profile",
1458            source_path,
1459        ),
1460    }
1461}
1462
1463fn is_safe_json_integer(value: &Value) -> bool {
1464    value
1465        .as_i64()
1466        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
1467        || value
1468            .as_u64()
1469            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
1470}
1471
1472fn is_safe_json_number(number: &serde_json::Number) -> bool {
1473    number
1474        .as_i64()
1475        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
1476        || number
1477            .as_u64()
1478            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
1479        || number.as_f64().is_some_and(|value| {
1480            value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
1481        })
1482}
1483
1484fn validate_wire_object(
1485    schema: &Value,
1486    value: &Value,
1487    path: &str,
1488    source_path: &Path,
1489) -> Result<(), CodegenError> {
1490    let Value::Object(properties) = value else {
1491        return invalid_wire_value(path, "expected an object", source_path);
1492    };
1493    let required = required_fields(schema);
1494    for field in required {
1495        if !properties.contains_key(&field) {
1496            return invalid_wire_value(
1497                &format!("{path}.{field}"),
1498                "required field is missing",
1499                source_path,
1500            );
1501        }
1502    }
1503    let declared = schema
1504        .get("properties")
1505        .and_then(Value::as_object)
1506        .cloned()
1507        .unwrap_or_default();
1508    for (field, field_value) in properties {
1509        if let Some(field_schema) = declared.get(field) {
1510            validate_wire_value_inner(
1511                field_schema,
1512                field_value,
1513                &format!("{path}.{field}"),
1514                source_path,
1515            )?;
1516            continue;
1517        }
1518        match schema.get("additionalProperties") {
1519            Some(Value::Bool(false)) => {
1520                return invalid_wire_value(
1521                    &format!("{path}.{field}"),
1522                    "additional property is not allowed",
1523                    source_path,
1524                );
1525            }
1526            Some(additional_schema) if !additional_schema.is_boolean() => {
1527                validate_wire_value_inner(
1528                    additional_schema,
1529                    field_value,
1530                    &format!("{path}.{field}"),
1531                    source_path,
1532                )?;
1533            }
1534            _ => {}
1535        }
1536    }
1537    Ok(())
1538}
1539
1540fn validate_wire_array(
1541    schema: &Value,
1542    value: &Value,
1543    path: &str,
1544    source_path: &Path,
1545) -> Result<(), CodegenError> {
1546    let Value::Array(values) = value else {
1547        return invalid_wire_value(path, "expected an array", source_path);
1548    };
1549    if let Some(items) = schema.get("items") {
1550        for (index, item) in values.iter().enumerate() {
1551            validate_wire_value_inner(items, item, &format!("{path}[{index}]"), source_path)?;
1552        }
1553    }
1554    let length = u64::try_from(values.len()).unwrap_or(u64::MAX);
1555    if let Some(minimum) = schema.get("minItems").and_then(Value::as_u64)
1556        && length < minimum
1557    {
1558        return invalid_wire_value(path, "array has fewer items than minItems", source_path);
1559    }
1560    if let Some(maximum) = schema.get("maxItems").and_then(Value::as_u64)
1561        && length > maximum
1562    {
1563        return invalid_wire_value(path, "array has more items than maxItems", source_path);
1564    }
1565    Ok(())
1566}
1567
1568fn validate_wire_string(
1569    schema: &Value,
1570    value: &Value,
1571    path: &str,
1572    source_path: &Path,
1573) -> Result<(), CodegenError> {
1574    let Value::String(value) = value else {
1575        return invalid_wire_value(path, "expected a string", source_path);
1576    };
1577    validate_string_format(schema, value, path, source_path)?;
1578    let length = u64::try_from(value.chars().count()).unwrap_or(u64::MAX);
1579    if let Some(minimum) = schema.get("minLength").and_then(Value::as_u64)
1580        && length < minimum
1581    {
1582        return invalid_wire_value(path, "string is shorter than minLength", source_path);
1583    }
1584    if let Some(maximum) = schema.get("maxLength").and_then(Value::as_u64)
1585        && length > maximum
1586    {
1587        return invalid_wire_value(path, "string is longer than maxLength", source_path);
1588    }
1589    Ok(())
1590}
1591
1592fn validate_numeric_constraint(
1593    schema: &Value,
1594    value: &Value,
1595    path: &str,
1596    source_path: &Path,
1597) -> Result<(), CodegenError> {
1598    let Some(number) = value.as_f64() else {
1599        return Ok(());
1600    };
1601    if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64)
1602        && number < minimum
1603    {
1604        return invalid_wire_value(path, "number is below minimum", source_path);
1605    }
1606    if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64)
1607        && number > maximum
1608    {
1609        return invalid_wire_value(path, "number is above maximum", source_path);
1610    }
1611    if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64)
1612        && number <= minimum
1613    {
1614        return invalid_wire_value(path, "number is not above exclusiveMinimum", source_path);
1615    }
1616    if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64)
1617        && number >= maximum
1618    {
1619        return invalid_wire_value(path, "number is not below exclusiveMaximum", source_path);
1620    }
1621    Ok(())
1622}
1623
1624fn validate_string_format(
1625    schema: &Value,
1626    value: &str,
1627    path: &str,
1628    source_path: &Path,
1629) -> Result<(), CodegenError> {
1630    let Some(format) = schema.get("format").and_then(Value::as_str) else {
1631        return Ok(());
1632    };
1633    let valid = match format {
1634        "int64" => is_signed_decimal(value),
1635        "uint64" => is_unsigned_decimal(value),
1636        "byte" => is_base64(value),
1637        "date-time" => is_rfc3339(value),
1638        "duration" => is_iso8601_duration(value),
1639        _ => false,
1640    };
1641    if valid {
1642        Ok(())
1643    } else {
1644        invalid_wire_value(
1645            path,
1646            &format!("string does not match portable `{format}` format"),
1647            source_path,
1648        )
1649    }
1650}
1651
1652fn invalid_wire_value<T>(path: &str, detail: &str, _source_path: &Path) -> Result<T, CodegenError> {
1653    Err(CodegenError::InvalidPortableValue {
1654        path: path.to_owned(),
1655        detail: detail.to_owned(),
1656    })
1657}
1658
1659fn is_signed_decimal(value: &str) -> bool {
1660    let digits = value.strip_prefix('-').unwrap_or(value);
1661    !digits.is_empty()
1662        && (digits == "0" || !digits.starts_with('0'))
1663        && digits.chars().all(|character| character.is_ascii_digit())
1664        && value.parse::<i64>().is_ok()
1665}
1666
1667fn is_unsigned_decimal(value: &str) -> bool {
1668    !value.is_empty()
1669        && (value == "0" || !value.starts_with('0'))
1670        && value.chars().all(|character| character.is_ascii_digit())
1671        && value.parse::<u64>().is_ok()
1672}
1673
1674fn is_base64(value: &str) -> bool {
1675    let bytes = value.as_bytes();
1676    if bytes.is_empty() {
1677        return true;
1678    }
1679    if !bytes.len().is_multiple_of(4) {
1680        return false;
1681    }
1682    let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count();
1683    if padding > 2 || bytes[..bytes.len() - padding].contains(&b'=') {
1684        return false;
1685    }
1686    for byte in &bytes[..bytes.len() - padding] {
1687        if base64_digit(*byte).is_none() {
1688            return false;
1689        }
1690    }
1691    let last = &bytes[bytes.len() - 4..];
1692    if padding == 1 {
1693        base64_digit(last[2]).is_some_and(|digit| digit.trailing_zeros() >= 2)
1694    } else if padding == 2 {
1695        base64_digit(last[1]).is_some_and(|digit| digit.trailing_zeros() >= 4)
1696    } else {
1697        base64_digit(last[2]).is_some() && base64_digit(last[3]).is_some()
1698    }
1699}
1700
1701fn base64_digit(byte: u8) -> Option<u8> {
1702    match byte {
1703        b'A'..=b'Z' => Some(byte - b'A'),
1704        b'a'..=b'z' => Some(byte - b'a' + 26),
1705        b'0'..=b'9' => Some(byte - b'0' + 52),
1706        b'+' => Some(62),
1707        b'/' => Some(63),
1708        _ => None,
1709    }
1710}
1711
1712fn is_rfc3339(value: &str) -> bool {
1713    let bytes = value.as_bytes();
1714    if bytes.len() < 20
1715        || !matches!(bytes.get(4), Some(b'-'))
1716        || !matches!(bytes.get(7), Some(b'-'))
1717        || !matches!(bytes.get(10), Some(b'T' | b't'))
1718        || !matches!(bytes.get(13), Some(b':'))
1719        || !matches!(bytes.get(16), Some(b':'))
1720    {
1721        return false;
1722    }
1723    let Some(month) = fixed_digits(bytes, 5, 2) else {
1724        return false;
1725    };
1726    let Some(day) = fixed_digits(bytes, 8, 2) else {
1727        return false;
1728    };
1729    let Some(hour) = fixed_digits(bytes, 11, 2) else {
1730        return false;
1731    };
1732    let Some(minute) = fixed_digits(bytes, 14, 2) else {
1733        return false;
1734    };
1735    let Some(second) = fixed_digits(bytes, 17, 2) else {
1736        return false;
1737    };
1738    let Some(year) = fixed_digits(bytes, 0, 4) else {
1739        return false;
1740    };
1741    let max_day = match month {
1742        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1743        4 | 6 | 9 | 11 => 30,
1744        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
1745        2 => 28,
1746        _ => return false,
1747    };
1748    if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
1749        return false;
1750    }
1751    let mut index = 19;
1752    if bytes.get(index) == Some(&b'.') {
1753        index += 1;
1754        let start = index;
1755        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
1756            index += 1;
1757        }
1758        if index == start {
1759            return false;
1760        }
1761    }
1762    match bytes.get(index) {
1763        Some(b'Z' | b'z') => index + 1 == bytes.len(),
1764        Some(b'+' | b'-') => {
1765            index += 1;
1766            let Some(offset_hour) = fixed_digits(bytes, index, 2) else {
1767                return false;
1768            };
1769            index += 2;
1770            if bytes.get(index) != Some(&b':') {
1771                return false;
1772            }
1773            index += 1;
1774            let Some(offset_minute) = fixed_digits(bytes, index, 2) else {
1775                return false;
1776            };
1777            offset_hour <= 23 && offset_minute <= 59 && index + 2 == bytes.len()
1778        }
1779        _ => false,
1780    }
1781}
1782
1783fn fixed_digits(bytes: &[u8], start: usize, length: usize) -> Option<u32> {
1784    let slice = bytes.get(start..start + length)?;
1785    slice.iter().try_fold(0_u32, |value, digit| {
1786        digit
1787            .is_ascii_digit()
1788            .then(|| value * 10 + u32::from(digit - b'0'))
1789    })
1790}
1791
1792fn is_iso8601_duration(value: &str) -> bool {
1793    let bytes = value.as_bytes();
1794    let mut index = usize::from(bytes.first() == Some(&b'-'));
1795    if bytes.get(index) != Some(&b'P') {
1796        return false;
1797    }
1798    index += 1;
1799    let mut in_time = false;
1800    let mut saw_component = false;
1801    let mut saw_time = false;
1802    while index < bytes.len() {
1803        if bytes[index] == b'T' {
1804            if in_time || index + 1 == bytes.len() {
1805                return false;
1806            }
1807            in_time = true;
1808            index += 1;
1809            continue;
1810        }
1811        let start = index;
1812        let mut separator_seen = false;
1813        while index < bytes.len()
1814            && (bytes[index].is_ascii_digit() || (!separator_seen && bytes[index] == b'.'))
1815        {
1816            separator_seen |= bytes[index] == b'.';
1817            index += 1;
1818        }
1819        if index == start || bytes.get(index).is_none() {
1820            return false;
1821        }
1822        let unit = bytes[index];
1823        let valid_unit = if in_time {
1824            matches!(unit, b'H' | b'M' | b'S')
1825        } else {
1826            matches!(unit, b'Y' | b'M' | b'W' | b'D')
1827        };
1828        if !valid_unit {
1829            return false;
1830        }
1831        if in_time {
1832            saw_time = true;
1833        }
1834        saw_component = true;
1835        index += 1;
1836    }
1837    saw_component && (!in_time || saw_time)
1838}
1839
1840fn validate_portable_value(value: &Value, path: String) -> Result<(), CodegenError> {
1841    match value {
1842        Value::Number(number) => {
1843            if !is_safe_json_number(number) {
1844                return Err(CodegenError::InvalidPortableValue {
1845                    path,
1846                    detail: "wide integers must use an explicit decimal-string format".to_owned(),
1847                });
1848            }
1849        }
1850        Value::Array(values) => {
1851            for (index, value) in values.iter().enumerate() {
1852                validate_portable_value(value, format!("{path}[{index}]"))?;
1853            }
1854        }
1855        Value::Object(values) => {
1856            for (key, value) in values {
1857                validate_portable_value(value, format!("{path}.{key}"))?;
1858            }
1859        }
1860        Value::Null | Value::Bool(_) | Value::String(_) => {}
1861    }
1862    Ok(())
1863}
1864
1865fn write_artifact(path: &Path, contents: &str) -> Result<(), CodegenError> {
1866    if let Some(parent) = path.parent() {
1867        fs::create_dir_all(parent).map_err(|source| CodegenError::Io {
1868            path: parent.to_path_buf(),
1869            source,
1870        })?;
1871    }
1872    fs::write(path, contents).map_err(|source| CodegenError::Io {
1873        path: path.to_path_buf(),
1874        source,
1875    })
1876}
1877
1878fn check_artifact(path: &Path, expected: &str) -> Result<(), CodegenError> {
1879    let actual = fs::read_to_string(path).map_err(|source| CodegenError::Io {
1880        path: path.to_path_buf(),
1881        source,
1882    })?;
1883    if actual == expected {
1884        Ok(())
1885    } else {
1886        Err(CodegenError::GeneratedArtifactDrift {
1887            path: path.to_path_buf(),
1888        })
1889    }
1890}
1891
1892/// Checks whether `new_path` is an additive, compatible evolution of
1893/// `old_path`. `true` means the same Capability series remains compatible.
1894pub fn lint_compatibility(old_path: &Path, new_path: &Path) -> Result<bool, CompatibilityError> {
1895    let old = load_descriptor(old_path).map_err(|error| CompatibilityError::BreakingChanges {
1896        changes: vec![error.to_string()],
1897    })?;
1898    let new = load_descriptor(new_path).map_err(|error| CompatibilityError::BreakingChanges {
1899        changes: vec![error.to_string()],
1900    })?;
1901    if old.capability_id != new.capability_id {
1902        return Err(CompatibilityError::IdentityChanged {
1903            from: old.capability_id,
1904            to: new.capability_id,
1905        });
1906    }
1907    if new.parsed_version <= old.parsed_version {
1908        return Err(CompatibilityError::VersionNotAdvanced {
1909            from: old.version,
1910            to: new.version,
1911        });
1912    }
1913    if new.parsed_version.major() != old.parsed_version.major() {
1914        return Err(CompatibilityError::BreakingChanges {
1915            changes: vec!["a breaking change must use a new Capability @major identity".to_owned()],
1916        });
1917    }
1918
1919    let mut changes = Vec::new();
1920    if old.portable != new.portable {
1921        changes.push("Descriptor portability changed".to_owned());
1922    }
1923    if old.cross_lane_transfer && !new.cross_lane_transfer {
1924        changes.push("Descriptor cross-lane transfer support was removed".to_owned());
1925    }
1926    let old_operations: BTreeMap<_, _> = old
1927        .operations
1928        .iter()
1929        .map(|operation| (operation.name.as_str(), operation))
1930        .collect();
1931    let new_operations: BTreeMap<_, _> = new
1932        .operations
1933        .iter()
1934        .map(|operation| (operation.name.as_str(), operation))
1935        .collect();
1936
1937    for (name, old_operation) in &old_operations {
1938        let Some(new_operation) = new_operations.get(name) else {
1939            changes.push(format!("Operation `{name}` was removed"));
1940            continue;
1941        };
1942        if old_operation.interaction != new_operation.interaction {
1943            changes.push(format!("Operation `{name}` changed interaction"));
1944        }
1945        compare_schema(
1946            &old_operation.request_schema,
1947            &new_operation.request_schema,
1948            &format!("Operation `{name}` request"),
1949            &mut changes,
1950        );
1951        compare_schema(
1952            &old_operation.response_schema,
1953            &new_operation.response_schema,
1954            &format!("Operation `{name}` response"),
1955            &mut changes,
1956        );
1957        compare_error_schema(
1958            &old_operation.domain_error_schema,
1959            &new_operation.domain_error_schema,
1960            &format!("Operation `{name}` Domain Error"),
1961            &mut changes,
1962        );
1963    }
1964
1965    if new.parsed_version.minor() == old.parsed_version.minor() {
1966        if contract_signature(&old) != contract_signature(&new) {
1967            changes.push("a patch release changed the observable contract".to_owned());
1968        }
1969        for name in new_operations
1970            .keys()
1971            .filter(|name| !old_operations.contains_key(**name))
1972        {
1973            changes.push(format!("Operation `{name}` was added in a patch release"));
1974        }
1975    }
1976    if new.parsed_version.minor() < old.parsed_version.minor() {
1977        changes.push("Descriptor minor version moved backwards".to_owned());
1978    }
1979
1980    if changes.is_empty() {
1981        Ok(true)
1982    } else {
1983        Err(CompatibilityError::BreakingChanges { changes })
1984    }
1985}
1986
1987fn contract_signature(descriptor: &Descriptor) -> String {
1988    let operations = descriptor
1989        .operations
1990        .iter()
1991        .map(|operation| {
1992            serde_json::json!({
1993                "name": operation.name,
1994                "interaction": operation.interaction,
1995                "request": operation.request_schema,
1996                "response": operation.response_schema,
1997                "domain_error": canonical_domain_error_schema(&operation.domain_error_schema),
1998            })
1999        })
2000        .collect::<Vec<_>>();
2001    canonical_json(&Value::Array(operations))
2002}
2003
2004fn canonical_domain_error_schema(schema: &Value) -> Value {
2005    let Some(variants) = schema.get("oneOf").and_then(Value::as_array) else {
2006        return schema.clone();
2007    };
2008    let mut variants = variants.clone();
2009    variants.sort_by(|left, right| {
2010        error_variant_code(left)
2011            .unwrap_or_default()
2012            .cmp(error_variant_code(right).unwrap_or_default())
2013    });
2014    let mut schema = schema.as_object().cloned().unwrap_or_default();
2015    schema.insert("oneOf".to_owned(), Value::Array(variants));
2016    Value::Object(schema)
2017}
2018
2019fn error_variant_code(variant: &Value) -> Option<&str> {
2020    variant.get("const").and_then(Value::as_str).or_else(|| {
2021        variant
2022            .get("properties")
2023            .and_then(Value::as_object)
2024            .and_then(|properties| properties.get("code"))
2025            .and_then(|code| code.get("const"))
2026            .and_then(Value::as_str)
2027    })
2028}
2029
2030fn compare_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2031    let old_nullable = is_nullable(old);
2032    let new_nullable = is_nullable(new);
2033    if old_nullable != new_nullable {
2034        changes.push(format!("{location} changed nullability"));
2035        return;
2036    }
2037    let old_type = schema_type_name(old);
2038    let new_type = schema_type_name(new);
2039    if old_type != new_type {
2040        changes.push(format!("{location} changed type"));
2041        return;
2042    }
2043    match old_type {
2044        Some("object") => {
2045            let old_properties = old.get("properties").and_then(Value::as_object);
2046            let new_properties = new.get("properties").and_then(Value::as_object);
2047            let (Some(old_properties), Some(new_properties)) = (old_properties, new_properties)
2048            else {
2049                if canonical_json(old) != canonical_json(new) {
2050                    changes.push(format!("{location} changed object shape"));
2051                }
2052                return;
2053            };
2054            let old_required = required_fields(old);
2055            let new_required = required_fields(new);
2056            for name in old_properties.keys() {
2057                let Some(new_schema) = new_properties.get(name) else {
2058                    changes.push(format!("{location} field `{name}` was removed"));
2059                    continue;
2060                };
2061                compare_schema(
2062                    &old_properties[name],
2063                    new_schema,
2064                    &format!("{location} field `{name}`"),
2065                    changes,
2066                );
2067            }
2068            for name in new_required.difference(&old_required) {
2069                changes.push(format!("{location} field `{name}` became required"));
2070            }
2071            for name in old_required.difference(&new_required) {
2072                changes.push(format!("{location} field `{name}` became optional"));
2073            }
2074            if old.get("additionalProperties") != new.get("additionalProperties") {
2075                changes.push(format!("{location} changed additional-properties policy"));
2076            }
2077            if schema_constraints_signature(old) != schema_constraints_signature(new) {
2078                changes.push(format!("{location} changed constraints"));
2079            }
2080        }
2081        Some("array") => {
2082            if let (Some(old_items), Some(new_items)) = (old.get("items"), new.get("items")) {
2083                compare_schema(old_items, new_items, &format!("{location} items"), changes);
2084            }
2085            if schema_constraints_signature(old) != schema_constraints_signature(new) {
2086                changes.push(format!("{location} changed constraints"));
2087            }
2088        }
2089        _ => {
2090            if canonical_json(old) != canonical_json(new) {
2091                changes.push(format!("{location} changed format or constraints"));
2092            }
2093        }
2094    }
2095}
2096
2097fn compare_error_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2098    let old_variants = error_variant_values(old);
2099    let new_variants = error_variant_values(new);
2100    if old_variants.is_empty() || new_variants.is_empty() {
2101        if canonical_json(old) != canonical_json(new) {
2102            changes.push(format!("{location} changed shape"));
2103        }
2104        return;
2105    }
2106    for (code, schema) in old_variants {
2107        match new_variants.get(&code) {
2108            None => changes.push(format!("{location} variant `{code}` was removed")),
2109            Some(new_schema) => compare_schema(
2110                &schema,
2111                new_schema,
2112                &format!("{location} variant `{code}`"),
2113                changes,
2114            ),
2115        }
2116    }
2117}
2118
2119fn error_variant_ir_definitions(schema: &Value) -> Vec<ErrorVariantIr> {
2120    let mut variants = schema
2121        .get("oneOf")
2122        .and_then(Value::as_array)
2123        .into_iter()
2124        .flatten()
2125        .filter_map(|variant| {
2126            if let Some(code) = variant.get("const").and_then(Value::as_str) {
2127                return Some(ErrorVariantIr {
2128                    code: code.to_owned(),
2129                    name: pascal_case(code),
2130                    structured: false,
2131                    payload: None,
2132                    payload_required: false,
2133                });
2134            }
2135            let object = variant.as_object()?;
2136            let code = object
2137                .get("properties")
2138                .and_then(Value::as_object)
2139                .and_then(|properties| properties.get("code"))
2140                .and_then(|code| code.get("const"))
2141                .and_then(Value::as_str)?;
2142            Some(ErrorVariantIr {
2143                code: code.to_owned(),
2144                name: pascal_case(code),
2145                structured: true,
2146                payload: object
2147                    .get("properties")
2148                    .and_then(Value::as_object)
2149                    .and_then(|properties| properties.get("payload"))
2150                    .map(type_ir_from_schema),
2151                payload_required: required_fields(variant).contains("payload"),
2152            })
2153        })
2154        .collect::<Vec<_>>();
2155    variants.sort_by(|left, right| left.code.cmp(&right.code));
2156    variants
2157}
2158
2159fn error_variant_values(schema: &Value) -> BTreeMap<String, Value> {
2160    let variants = schema
2161        .get("oneOf")
2162        .and_then(Value::as_array)
2163        .into_iter()
2164        .flatten();
2165    variants
2166        .filter_map(|variant| {
2167            let code = if let Some(code) = variant.get("const").and_then(Value::as_str) {
2168                code.to_owned()
2169            } else {
2170                variant
2171                    .get("properties")
2172                    .and_then(Value::as_object)
2173                    .and_then(|properties| properties.get("code"))
2174                    .and_then(|code| code.get("const"))
2175                    .and_then(Value::as_str)
2176                    .map(ToOwned::to_owned)?
2177            };
2178            Some((code, variant.clone()))
2179        })
2180        .collect()
2181}
2182
2183fn required_fields(schema: &Value) -> BTreeSet<String> {
2184    schema
2185        .get("required")
2186        .and_then(Value::as_array)
2187        .into_iter()
2188        .flatten()
2189        .filter_map(Value::as_str)
2190        .map(ToOwned::to_owned)
2191        .collect()
2192}
2193
2194fn is_nullable(schema: &Value) -> bool {
2195    schema
2196        .get("type")
2197        .and_then(Value::as_array)
2198        .is_some_and(|types| types.iter().any(|value| value == "null"))
2199        || schema
2200            .get("anyOf")
2201            .and_then(Value::as_array)
2202            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
2203        || schema
2204            .get("oneOf")
2205            .and_then(Value::as_array)
2206            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
2207}
2208
2209fn is_null_schema(schema: &Value) -> bool {
2210    schema.get("type").is_some_and(|value| value == "null")
2211        || schema.get("const").is_some_and(Value::is_null)
2212}
2213
2214fn schema_type_name(schema: &Value) -> Option<&str> {
2215    schema.get("type").and_then(|value| match value {
2216        Value::String(value) => Some(value.as_str()),
2217        Value::Array(values) => values
2218            .iter()
2219            .find_map(Value::as_str)
2220            .filter(|value| *value != "null"),
2221        _ => None,
2222    })
2223}
2224
2225fn canonical_json(value: &Value) -> String {
2226    serde_json::to_string(value).expect("JSON values are serializable")
2227}
2228
2229fn schema_constraints_signature(schema: &Value) -> String {
2230    let mut schema = schema.as_object().cloned().unwrap_or_default();
2231    for key in [
2232        "type",
2233        "properties",
2234        "required",
2235        "additionalProperties",
2236        "items",
2237    ] {
2238        schema.remove(key);
2239    }
2240    canonical_json(&Value::Object(schema))
2241}
2242
2243fn is_identifier(value: &str) -> bool {
2244    let mut characters = value.chars();
2245    characters
2246        .next()
2247        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2248        && characters
2249            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
2250}
2251
2252fn is_rust_member_name(value: &str) -> bool {
2253    !matches!(snake_case(value).as_str(), "self" | "super" | "crate")
2254}
2255
2256fn is_rust_type_identifier(value: &str) -> bool {
2257    let mut characters = value.chars();
2258    let valid = characters
2259        .next()
2260        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2261        && characters.all(|character| character.is_ascii_alphanumeric() || character == '_');
2262    valid && value != "Self"
2263}
2264
2265fn is_generated_type_name(value: &str) -> bool {
2266    is_rust_type_identifier(value)
2267        && !matches!(
2268            value,
2269            "Self"
2270                | "String"
2271                | "Int64"
2272                | "Uint64"
2273                | "Bytes"
2274                | "Timestamp"
2275                | "Duration"
2276                | "OptionalValue"
2277                | "UnknownDomainError"
2278                | "InvocationContext"
2279                | "ModuleDependencies"
2280                | "NativeRequestEndpoint"
2281                | "NativeRequestHandle"
2282                | "RequestCapability"
2283                | "RuntimeFailure"
2284                | "LocalBoxFuture"
2285        )
2286}
2287
2288fn is_generated_enum_variant(value: &str) -> bool {
2289    is_rust_type_identifier(value)
2290}
2291
2292fn pascal_case(value: &str) -> String {
2293    let mut output = String::new();
2294    for part in value.split(|character: char| !character.is_ascii_alphanumeric()) {
2295        if part.is_empty() || part.chars().all(char::is_numeric) {
2296            continue;
2297        }
2298        let mut chars = part.chars();
2299        if let Some(first) = chars.next() {
2300            output.extend(first.to_uppercase());
2301            output.push_str(chars.as_str());
2302        }
2303    }
2304    if output.is_empty() {
2305        "Value".to_owned()
2306    } else if output
2307        .chars()
2308        .next()
2309        .is_some_and(|character| character.is_ascii_digit())
2310    {
2311        format!("Value{output}")
2312    } else {
2313        output
2314    }
2315}
2316
2317fn snake_case(value: &str) -> String {
2318    let mut output = String::new();
2319    for (index, character) in value.chars().enumerate() {
2320        if character.is_ascii_alphanumeric() {
2321            if character.is_ascii_uppercase() && index != 0 {
2322                output.push('_');
2323            }
2324            output.push(character.to_ascii_lowercase());
2325        } else if !output.ends_with('_') {
2326            output.push('_');
2327        }
2328    }
2329    let output = output.trim_matches('_').to_owned();
2330    if output.is_empty() {
2331        "value".to_owned()
2332    } else {
2333        output
2334    }
2335}
2336
2337fn screaming_snake_case(value: &str) -> String {
2338    snake_case(value).to_ascii_uppercase()
2339}
2340
2341fn rust_field_name(value: &str) -> String {
2342    let name = snake_case(value);
2343    if matches!(name.as_str(), "self" | "super" | "crate") {
2344        return format!("{name}_");
2345    }
2346    if matches!(
2347        name.as_str(),
2348        "as" | "break"
2349            | "const"
2350            | "continue"
2351            | "crate"
2352            | "else"
2353            | "enum"
2354            | "extern"
2355            | "false"
2356            | "fn"
2357            | "for"
2358            | "if"
2359            | "impl"
2360            | "in"
2361            | "let"
2362            | "loop"
2363            | "match"
2364            | "mod"
2365            | "move"
2366            | "mut"
2367            | "pub"
2368            | "ref"
2369            | "return"
2370            | "self"
2371            | "Self"
2372            | "static"
2373            | "struct"
2374            | "super"
2375            | "trait"
2376            | "true"
2377            | "type"
2378            | "unsafe"
2379            | "use"
2380            | "where"
2381            | "while"
2382            | "async"
2383            | "await"
2384            | "dyn"
2385    ) {
2386        format!("r#{name}")
2387    } else {
2388        name
2389    }
2390}
2391
2392fn quote_string(value: &str) -> String {
2393    serde_json::to_string(value).expect("string is serializable")
2394}
2395
2396fn typescript_property_name(value: &str) -> String {
2397    let mut characters = value.chars();
2398    let valid_identifier = characters
2399        .next()
2400        .is_some_and(|character| character.is_ascii_alphabetic() || matches!(character, '_' | '$'))
2401        && characters
2402            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$'));
2403    if valid_identifier {
2404        value.to_owned()
2405    } else {
2406        quote_string(value)
2407    }
2408}
2409
2410struct RustTypes {
2411    declarations: Vec<String>,
2412    declared: BTreeSet<String>,
2413}
2414
2415impl RustTypes {
2416    fn new() -> Self {
2417        Self {
2418            declarations: Vec::new(),
2419            declared: BTreeSet::new(),
2420        }
2421    }
2422
2423    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
2424        if !self.declared.insert(name.to_owned()) {
2425            return name.to_owned();
2426        }
2427        let placeholder = self.declarations.len();
2428        self.declarations.push(String::new());
2429        let mut rendered_fields = Vec::new();
2430        for field in fields {
2431            let type_name = self.type_for_non_null(
2432                field.ty.non_null(),
2433                &format!("{name}{}", pascal_case(&field.name)),
2434            );
2435            let field_type = if field.required {
2436                if field.ty.is_nullable() {
2437                    format!("Option<{type_name}>")
2438                } else {
2439                    type_name
2440                }
2441            } else if field.ty.is_nullable() {
2442                format!("OptionalValue<{type_name}>")
2443            } else {
2444                format!("Option<{type_name}>")
2445            };
2446            let mut attributes = vec![format!(
2447                "    #[serde(rename = {})]",
2448                quote_string(&field.name)
2449            )];
2450            if !field.required {
2451                if field.ty.is_nullable() {
2452                    attributes.push("    #[serde(default)]".to_owned());
2453                    attributes
2454                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
2455                    attributes.push(
2456                        "    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_optional_value\")]"
2457                            .to_owned(),
2458                    );
2459                } else {
2460                    attributes
2461                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
2462                }
2463            }
2464            if field.required {
2465                attributes
2466                    .push("    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_required\")]".to_owned());
2467            }
2468            rendered_fields.push(format!(
2469                "{}\n    pub {}: {field_type},",
2470                attributes.join("\n"),
2471                rust_field_name(&field.name)
2472            ));
2473        }
2474        let debug_impl = rust_debug::render(
2475            name,
2476            fields.iter().map(|field| {
2477                (
2478                    field.name.as_str(),
2479                    rust_field_name(&field.name),
2480                    field.sensitive,
2481                )
2482            }),
2483        );
2484        let derives = if debug_impl.is_some() {
2485            "Clone, PartialEq, serde::Serialize, serde::Deserialize"
2486        } else {
2487            "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
2488        };
2489        self.declarations[placeholder] = format!(
2490            "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
2491            rendered_fields.join("\n"),
2492            debug_impl = debug_impl.unwrap_or_default(),
2493        );
2494        name.to_owned()
2495    }
2496
2497    fn enum_type(&mut self, name: &str, values: &[String]) -> String {
2498        if !self.declared.insert(name.to_owned()) {
2499            return name.to_owned();
2500        }
2501        let variants = values
2502            .iter()
2503            .map(|value| {
2504                format!(
2505                    "    #[serde(rename = {})]\n    {},",
2506                    quote_string(value),
2507                    pascal_case(value)
2508                )
2509            })
2510            .collect::<Vec<_>>();
2511        self.declarations.push(format!(
2512            "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
2513            variants.join("\n")
2514        ));
2515        name.to_owned()
2516    }
2517
2518    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2519        let base = self.type_for_non_null(ty.non_null(), nested_name);
2520        if ty.is_nullable() {
2521            format!("Option<{base}>")
2522        } else {
2523            base
2524        }
2525    }
2526
2527    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2528        match ty {
2529            TypeIr::Any => "serde_json::Value".to_owned(),
2530            TypeIr::String => "String".to_owned(),
2531            TypeIr::Enum(values) => self.enum_type(nested_name, values),
2532            TypeIr::Int64 => "Int64".to_owned(),
2533            TypeIr::Uint64 => "Uint64".to_owned(),
2534            TypeIr::Bytes => "Bytes".to_owned(),
2535            TypeIr::Timestamp => "Timestamp".to_owned(),
2536            TypeIr::Duration => "Duration".to_owned(),
2537            TypeIr::Integer => "i64".to_owned(),
2538            TypeIr::Number => "f64".to_owned(),
2539            TypeIr::Boolean => "bool".to_owned(),
2540            TypeIr::Null => "()".to_owned(),
2541            TypeIr::Array(items) => {
2542                format!(
2543                    "Vec<{}>",
2544                    self.type_for(items, &format!("{nested_name}Item"))
2545                )
2546            }
2547            TypeIr::Object { fields, additional } => {
2548                if fields.is_empty() {
2549                    match additional {
2550                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
2551                        ObjectAdditionalIr::Any => {
2552                            "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
2553                        }
2554                        ObjectAdditionalIr::Typed(values) => format!(
2555                            "std::collections::BTreeMap<String, {}>",
2556                            self.type_for(values, &format!("{nested_name}Value"))
2557                        ),
2558                    }
2559                } else {
2560                    self.object(nested_name, fields)
2561                }
2562            }
2563            TypeIr::Nullable(inner) => {
2564                format!("Option<{}>", self.type_for_non_null(inner, nested_name))
2565            }
2566        }
2567    }
2568}
2569
2570struct TypeScriptTypes {
2571    declarations: Vec<String>,
2572    declared: BTreeSet<String>,
2573}
2574
2575impl TypeScriptTypes {
2576    fn new() -> Self {
2577        Self {
2578            declarations: Vec::new(),
2579            declared: BTreeSet::new(),
2580        }
2581    }
2582
2583    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
2584        if !self.declared.insert(name.to_owned()) {
2585            return name.to_owned();
2586        }
2587        let placeholder = self.declarations.len();
2588        self.declarations.push(String::new());
2589        let mut rendered_fields = Vec::new();
2590        for field in fields {
2591            let type_name = self.type_for_non_null(
2592                field.ty.non_null(),
2593                &format!("{name}{}", pascal_case(&field.name)),
2594            );
2595            let optional = if field.required { "" } else { "?" };
2596            let field_type = if field.ty.is_nullable() {
2597                format!("{type_name} | null")
2598            } else {
2599                type_name
2600            };
2601            rendered_fields.push(format!(
2602                "  {}{optional}: {field_type};",
2603                typescript_property_name(&field.name)
2604            ));
2605        }
2606        self.declarations[placeholder] = format!(
2607            "export interface {name} {{\n{}\n}}\n",
2608            rendered_fields.join("\n")
2609        );
2610        name.to_owned()
2611    }
2612
2613    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2614        let base = self.type_for_non_null(ty.non_null(), nested_name);
2615        if ty.is_nullable() {
2616            format!("{base} | null")
2617        } else {
2618            base
2619        }
2620    }
2621
2622    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2623        match ty {
2624            TypeIr::Any => "unknown".to_owned(),
2625            TypeIr::String => "string".to_owned(),
2626            TypeIr::Int64 => "Int64".to_owned(),
2627            TypeIr::Uint64 => "Uint64".to_owned(),
2628            TypeIr::Bytes => "Bytes".to_owned(),
2629            TypeIr::Timestamp => "Timestamp".to_owned(),
2630            TypeIr::Duration => "Duration".to_owned(),
2631            TypeIr::Integer | TypeIr::Number => "number".to_owned(),
2632            TypeIr::Boolean => "boolean".to_owned(),
2633            TypeIr::Null => "null".to_owned(),
2634            TypeIr::Enum(values) => values
2635                .iter()
2636                .map(|value| quote_string(value))
2637                .collect::<Vec<_>>()
2638                .join(" | "),
2639            TypeIr::Array(items) => {
2640                format!(
2641                    "Array<{}>",
2642                    self.type_for(items, &format!("{nested_name}Item"))
2643                )
2644            }
2645            TypeIr::Object { fields, additional } => {
2646                if fields.is_empty() {
2647                    match additional {
2648                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
2649                        ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
2650                        ObjectAdditionalIr::Typed(values) => format!(
2651                            "Record<string, {}>",
2652                            self.type_for(values, &format!("{nested_name}Value"))
2653                        ),
2654                    }
2655                } else {
2656                    self.object(nested_name, fields)
2657                }
2658            }
2659            TypeIr::Nullable(inner) => {
2660                format!("{} | null", self.type_for_non_null(inner, nested_name))
2661            }
2662        }
2663    }
2664}
2665
2666#[allow(clippy::too_many_lines)]
2667fn generate_rust(contract: &ContractIr) -> String {
2668    let capability_name = pascal_case(
2669        contract
2670            .capability_id
2671            .split('@')
2672            .next()
2673            .and_then(|identity| identity.rsplit('.').next())
2674            .unwrap_or("Capability"),
2675    );
2676    let capability_const = screaming_snake_case(&capability_name);
2677    let mut types = RustTypes::new();
2678    let mut operation_rows = Vec::new();
2679    let mut stream_operation_rows = Vec::new();
2680    let mut event_operation_rows = Vec::new();
2681    let mut operation_markers = Vec::new();
2682    let mut provider_methods = Vec::new();
2683    let mut endpoint_arms = Vec::new();
2684    let mut stream_endpoint_arms = Vec::new();
2685    let mut event_endpoint_arms = Vec::new();
2686    let mut client_fields = Vec::new();
2687    let mut client_initializers = Vec::new();
2688    let mut client_methods = Vec::new();
2689    let mut invocation_errors = Vec::new();
2690    let mut error_codecs = Vec::new();
2691    let mut wire_codecs = Vec::new();
2692
2693    for operation in &contract.operations {
2694        let operation_name = pascal_case(&operation.name);
2695        let request_name = format!("{operation_name}Request");
2696        let response_name = format!("{operation_name}Response");
2697        let error_name = format!("{operation_name}Error");
2698        let marker_name = if contract.operations.len() == 1 {
2699            capability_name.clone()
2700        } else {
2701            format!("{capability_name}{operation_name}")
2702        };
2703        let request_type = types.type_for(&operation.request, &request_name);
2704        let response_type = types.type_for(&operation.response, &response_name);
2705        let known_errors = &operation.domain_errors;
2706        let error_definition = if known_errors.is_empty() {
2707            format!(
2708                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n    Unknown(UnknownDomainError),\n}}\n"
2709            )
2710        } else {
2711            let variants = known_errors
2712                .iter()
2713                .map(|variant| {
2714                    if let Some(payload) = &variant.payload {
2715                        let payload_name = format!("{error_name}{}Payload", variant.name);
2716                        let payload_type =
2717                            types.type_for_non_null(payload.non_null(), &payload_name);
2718                        let payload_type = if variant.payload_required {
2719                            if payload.is_nullable() {
2720                                format!("Option<{payload_type}>")
2721                            } else {
2722                                payload_type
2723                            }
2724                        } else if payload.is_nullable() {
2725                            format!("OptionalValue<{payload_type}>")
2726                        } else {
2727                            format!("Option<{payload_type}>")
2728                        };
2729                        format!("    {} {{ payload: {payload_type} }},", variant.name)
2730                    } else {
2731                        format!("    {},", variant.name)
2732                    }
2733                })
2734                .chain(std::iter::once(
2735                    "    Unknown(UnknownDomainError),".to_owned(),
2736                ))
2737                .collect::<Vec<_>>();
2738            format!(
2739                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
2740                variants.join("\n")
2741            )
2742        };
2743        types.declarations.push(error_definition);
2744        error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
2745        wire_codecs.push(generate_rust_wire_codecs(
2746            &operation.name,
2747            &request_type,
2748            &response_type,
2749            &error_name,
2750        ));
2751        if operation.interaction == "event" {
2752            wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
2753        }
2754        let operation_const = screaming_snake_case(&operation.name);
2755        operation_markers.push(match operation.interaction.as_str() {
2756            "request" => {
2757                let provider_method = rust_field_name(&operation.name);
2758                format!(
2759            "#[derive(Debug)]\npub struct {marker_name};\nimpl RequestCapability for {marker_name} {{\n    type Request = {request_type};\n    type Response = {response_type};\n    type DomainError = {error_name};\n    const ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n\n    fn invoke_native(endpoint: &dyn NativeRequestEndpoint, operation: &str, request: Self::Request, context: InvocationContext) -> NativeRequestFuture<Self> {{\n        if operation != {operation_const}_OPERATION {{\n            return lenso_kernel::invoke_typed_or_erased_native_request::<Self>(endpoint, operation, request, context);\n        }}\n        let Some(typed_endpoint) = endpoint\n            .typed_endpoint()\n            .and_then(|endpoint| endpoint.downcast_ref::<{capability_name}RequestEndpoint>())\n        else {{\n            return lenso_kernel::invoke_typed_or_erased_native_request::<Self>(endpoint, operation, request, context);\n        }};\n        Rc::clone(&typed_endpoint.provider).{provider_method}(context, request)\n    }}\n}}\n"
2760                )
2761            }
2762            "stream" => format!(
2763                "#[derive(Debug)]\npub struct {marker_name};\npub type {marker_name}Event = StreamEvent<{response_type}, {error_name}>;\nimpl StreamCapability for {marker_name} {{\n    type OpenRequest = {request_type};\n    type Message = {response_type};\n    type DomainError = {error_name};\n    const ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n}}\n"
2764            ),
2765            "event" => format!(
2766                "#[derive(Debug)]\npub struct {marker_name};\nimpl EventCapability for {marker_name} {{\n    type Event = {request_type};\n    const ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n}}\n"
2767            ),
2768            _ => unreachable!("Descriptor validation restricts interactions"),
2769        });
2770        if operation.interaction == "request" {
2771            let invocation_error_name = if contract.operations.len() == 1 {
2772                format!("{capability_name}InvocationError")
2773            } else {
2774                format!("{capability_name}{operation_name}InvocationError")
2775            };
2776            operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
2777            provider_methods.push(format!(
2778                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
2779                rust_field_name(&operation.name),
2780            ));
2781            endpoint_arms.push(format!(
2782                "            {operation_const}_OPERATION => {{\n                let Ok(request) = request.downcast::<{request_type}>() else {{\n                    return Box::pin(futures::future::ready(Err(RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }})));\n                }};\n                let invocation = Rc::clone(&self.provider).{}(context, *request);\n                Box::pin(async move {{\n                    invocation.await.map(|result| {{\n                        result\n                            .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                            .map_err(|error| Box::new(error) as Box<dyn std::any::Any>)\n                    }})\n                }})\n            }}",
2783                rust_field_name(&operation.name),
2784            ));
2785            let field = rust_field_name(&operation.name);
2786            client_fields.push(format!("    {field}: NativeRequestHandle<{marker_name}>,"));
2787            client_initializers.push(format!(
2788                "            {field}: dependencies.one::<{marker_name}>()?,"
2789            ));
2790            client_methods.push(format!(
2791                "    pub async fn {field}(&self, request: {request_type}) -> Result<{response_type}, {invocation_error_name}> {{\n        self.{field}.invoke({}_OPERATION, request).await\n            .map_err({invocation_error_name}::Runtime)?\n            .map_err({invocation_error_name}::Domain)\n    }}\n\n    pub async fn {field}_with_context(&self, context: InvocationContext, request: {request_type}) -> Result<{response_type}, {invocation_error_name}> {{\n        self.{field}.invoke_with_context({}_OPERATION, context, request).await\n            .map_err({invocation_error_name}::Runtime)?\n            .map_err({invocation_error_name}::Domain)\n    }}",
2792                screaming_snake_case(&operation.name),
2793                screaming_snake_case(&operation.name),
2794            ));
2795            invocation_errors.push(format!(
2796                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
2797            ));
2798        } else if operation.interaction == "stream" {
2799            let invocation_error_name = if contract.operations.len() == 1 {
2800                format!("{capability_name}InvocationError")
2801            } else {
2802                format!("{capability_name}{operation_name}InvocationError")
2803            };
2804            stream_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
2805            provider_methods.push(format!(
2806                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
2807                rust_field_name(&operation.name)
2808            ));
2809            stream_endpoint_arms.push(format!(
2810                "            {operation_const}_OPERATION => {{\n                let Ok(request) = request.downcast::<{request_type}>() else {{\n                    return Box::pin(futures::future::ready(Err(RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }})));\n                }};\n                let provider = Rc::clone(&self.provider);\n                Box::pin(async move {{\n                    match provider.{}(context, *request).await {{\n                        Ok(value) => Ok(Ok(value as Box<dyn NativeStreamSession>)),\n                        Err({invocation_error_name}::Domain(error)) => Ok(Err(Box::new(error) as Box<dyn std::any::Any>)),\n                        Err({invocation_error_name}::Runtime(error)) => Err(error),\n                    }}\n                }})\n            }}",
2811                rust_field_name(&operation.name),
2812            ));
2813            let field = rust_field_name(&operation.name);
2814            client_fields.push(format!("    {field}: NativeStreamHandle<{marker_name}>,"));
2815            client_initializers.push(format!(
2816                "            {field}: dependencies.one_stream::<{marker_name}>()?,"
2817            ));
2818            client_methods.push(format!(
2819                "    pub async fn {field}(&self, request: {request_type}) -> Result<NativeStream<{marker_name}>, {invocation_error_name}> {{\n        self.{field}.open({operation_const}_OPERATION, request).await\n            .map_err({invocation_error_name}::Runtime)?\n            .map_err({invocation_error_name}::Domain)\n    }}\n\n    pub async fn {field}_with_context(&self, context: InvocationContext, request: {request_type}) -> Result<NativeStream<{marker_name}>, {invocation_error_name}> {{\n        self.{field}.open_with_context({operation_const}_OPERATION, context, request).await\n            .map_err({invocation_error_name}::Runtime)?\n            .map_err({invocation_error_name}::Domain)\n    }}"
2820            ));
2821            invocation_errors.push(format!(
2822                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
2823            ));
2824        } else {
2825            event_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
2826            provider_methods.push(format!(
2827                "    fn {}(&self, context: InvocationContext, event: {request_type});",
2828                rust_field_name(&operation.name)
2829            ));
2830            event_endpoint_arms.push(format!(
2831                "            {operation_const}_OPERATION => {{\n                let Ok(event) = event.downcast::<{request_type}>() else {{\n                    return Box::pin(futures::future::ready(Err(RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }})));\n                }};\n                let provider = Rc::clone(&self.provider);\n                Box::pin(async move {{\n                    provider.{}(context, *event);\n                    Ok(())\n                }})\n            }}",
2832                rust_field_name(&operation.name),
2833            ));
2834            let field = rust_field_name(&operation.name);
2835            client_fields.push(format!("    {field}: NativeEventHandle<{marker_name}>,"));
2836            client_initializers.push(format!(
2837                "            {field}: dependencies.many_event::<{marker_name}>()?,"
2838            ));
2839            client_methods.push(format!(
2840                "    pub async fn {field}(&self, event: {request_type}) -> Vec<EventPublishResult> {{\n        self.{field}.publish({operation_const}_OPERATION, event).await\n    }}\n\n    pub async fn {field}_with_context(&self, context: InvocationContext, event: {request_type}) -> Vec<EventPublishResult> {{\n        self.{field}.publish_with_context({operation_const}_OPERATION, context, event).await\n    }}"
2841            ));
2842        }
2843    }
2844
2845    let request_endpoint_impl = if operation_rows.is_empty() {
2846        String::new()
2847    } else {
2848        format!(
2849            "impl<P: {capability_name}Provider> NativeRequestEndpoint for {capability_name}Endpoint<P> {{\n    fn capability_id(&self) -> &'static str {{ CAPABILITY_ID }}\n    fn descriptor_version(&self) -> &'static str {{ DESCRIPTOR_VERSION }}\n    fn operations(&self) -> &'static [&'static str] {{ &[\n{}    ] }}\n    fn typed_endpoint(&self) -> Option<&dyn std::any::Any> {{ Some(&self.request_endpoint) }}\n    fn invoke(&self, operation: &str, request: Box<dyn std::any::Any>, context: InvocationContext) -> LocalBoxFuture<'static, Result<Result<Box<dyn std::any::Any>, Box<dyn std::any::Any>>, RuntimeFailure>> {{\n        match operation {{\n{}\n            _ => Box::pin(futures::future::ready(Err(RuntimeFailure::UnknownOperation {{ capability: CAPABILITY_ID, operation: operation.to_owned() }}))),\n        }}\n    }}\n}}\n\n",
2850            operation_rows.concat(),
2851            endpoint_arms.join(",\n")
2852        )
2853    };
2854    let stream_endpoint_impl = if stream_operation_rows.is_empty() {
2855        String::new()
2856    } else {
2857        format!(
2858            "impl<P: {capability_name}Provider> NativeStreamEndpoint for {capability_name}Endpoint<P> {{\n    fn capability_id(&self) -> &'static str {{ CAPABILITY_ID }}\n    fn descriptor_version(&self) -> &'static str {{ DESCRIPTOR_VERSION }}\n    fn operations(&self) -> &'static [&'static str] {{ &[\n{}    ] }}\n    fn open(&self, operation: &str, request: Box<dyn std::any::Any>, context: InvocationContext) -> LocalBoxFuture<'static, Result<Result<Box<dyn NativeStreamSession>, Box<dyn std::any::Any>>, RuntimeFailure>> {{\n        match operation {{\n{}\n            _ => Box::pin(futures::future::ready(Err(RuntimeFailure::UnknownOperation {{ capability: CAPABILITY_ID, operation: operation.to_owned() }}))),\n        }}\n    }}\n}}\n\n",
2859            stream_operation_rows.concat(),
2860            stream_endpoint_arms.join(",\n")
2861        )
2862    };
2863    let event_endpoint_impl = if event_operation_rows.is_empty() {
2864        String::new()
2865    } else {
2866        format!(
2867            "impl<P: {capability_name}Provider> NativeEventEndpoint for {capability_name}Endpoint<P> {{\n    fn capability_id(&self) -> &'static str {{ CAPABILITY_ID }}\n    fn descriptor_version(&self) -> &'static str {{ DESCRIPTOR_VERSION }}\n    fn operations(&self) -> &'static [&'static str] {{ &[\n{}    ] }}\n    fn publish(&self, operation: &str, event: Box<dyn std::any::Any>, context: InvocationContext) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>> {{\n        match operation {{\n{}\n            _ => Box::pin(futures::future::ready(Err(RuntimeFailure::UnknownOperation {{ capability: CAPABILITY_ID, operation: operation.to_owned() }}))),\n        }}\n    }}\n}}\n\n",
2868            event_operation_rows.concat(),
2869            event_endpoint_arms.join(",\n")
2870        )
2871    };
2872    let mut output = String::new();
2873    output.push_str(GENERATED_HEADER);
2874    let has_request_operations = !operation_rows.is_empty();
2875    let has_stream_operations = !stream_operation_rows.is_empty();
2876    let has_event_operations = !event_operation_rows.is_empty();
2877    let mut kernel_imports = vec!["InvocationContext", "ModuleDependencies", "RuntimeFailure"];
2878    if has_request_operations {
2879        kernel_imports.extend([
2880            "NativeRequestFuture",
2881            "NativeRequestEndpoint",
2882            "NativeRequestHandle",
2883            "RequestCapability",
2884        ]);
2885    }
2886    if has_stream_operations {
2887        kernel_imports.extend([
2888            "NativeStream",
2889            "NativeStreamEndpoint",
2890            "NativeStreamHandle",
2891            "NativeStreamSession",
2892            "StreamCapability",
2893            "StreamEvent",
2894        ]);
2895    }
2896    if has_event_operations {
2897        kernel_imports.extend([
2898            "EventCapability",
2899            "EventPublishResult",
2900            "NativeEventEndpoint",
2901            "NativeEventHandle",
2902        ]);
2903    }
2904    kernel_imports.sort_unstable();
2905    writeln!(
2906        output,
2907        "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
2908        kernel_imports.join(", ")
2909    )
2910    .expect("writing to a String cannot fail");
2911    writeln!(
2912        output,
2913        "pub const CAPABILITY_ID: &str = {};",
2914        quote_string(&contract.capability_id)
2915    )
2916    .expect("writing to a String cannot fail");
2917    writeln!(
2918        output,
2919        "pub const DESCRIPTOR_VERSION: &str = {};",
2920        quote_string(&contract.version)
2921    )
2922    .expect("writing to a String cannot fail");
2923    writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
2924        .expect("writing to a String cannot fail");
2925    writeln!(
2926        output,
2927        "pub const CROSS_LANE_TRANSFER: bool = {};",
2928        contract.cross_lane_transfer
2929    )
2930    .expect("writing to a String cannot fail");
2931    writeln!(
2932        output,
2933        "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
2934    )
2935    .expect("writing to a String cannot fail");
2936    write!(
2937        output,
2938        "pub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\n\n"
2939    )
2940    .expect("writing to a String cannot fail");
2941    for operation in &contract.operations {
2942        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
2943        writeln!(
2944            output,
2945            "pub const {operation_const}: &str = {};",
2946            quote_string(&operation.name)
2947        )
2948        .expect("writing to a String cannot fail");
2949    }
2950    writeln!(
2951        output,
2952        "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
2953        rust_runtime_types(contract).join(", ")
2954    )
2955    .expect("writing to a String cannot fail");
2956    for declaration in types.declarations {
2957        output.push_str(&declaration);
2958        output.push('\n');
2959    }
2960    for marker in operation_markers {
2961        output.push_str(&marker);
2962        output.push('\n');
2963    }
2964    for codec in error_codecs {
2965        output.push_str(&codec);
2966        output.push('\n');
2967    }
2968    for codec in wire_codecs {
2969        output.push_str(&codec);
2970        output.push('\n');
2971    }
2972    write!(
2973        output,
2974        "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
2975        provider_methods.join("\n")
2976    )
2977    .expect("writing to a String cannot fail");
2978    if has_request_operations {
2979        write!(
2980            output,
2981            "#[derive(Debug)]\nstruct {capability_name}RequestEndpoint {{ provider: Rc<dyn {capability_name}Provider> }}\n\n#[derive(Debug)]\npub struct {capability_name}Endpoint<P: {capability_name}Provider> {{ provider: Rc<P>, request_endpoint: {capability_name}RequestEndpoint }}\nimpl<P: {capability_name}Provider> {capability_name}Endpoint<P> {{\n    pub fn new(provider: P) -> Self {{\n        let provider = Rc::new(provider);\n        let request_provider: Rc<dyn {capability_name}Provider> = provider.clone();\n        Self {{ provider, request_endpoint: {capability_name}RequestEndpoint {{ provider: request_provider }} }}\n    }}\n}}\n\n"
2982        )
2983        .expect("writing to a String cannot fail");
2984    } else {
2985        write!(
2986            output,
2987            "#[derive(Debug)]\npub struct {capability_name}Endpoint<P: {capability_name}Provider> {{ provider: Rc<P> }}\nimpl<P: {capability_name}Provider> {capability_name}Endpoint<P> {{\n    pub fn new(provider: P) -> Self {{ Self {{ provider: Rc::new(provider) }} }}\n}}\n\n"
2988        )
2989        .expect("writing to a String cannot fail");
2990    }
2991    output.push_str(&request_endpoint_impl);
2992    output.push_str(&stream_endpoint_impl);
2993    output.push_str(&event_endpoint_impl);
2994    let new_method = if contract.operations.len() == 1 {
2995        let field = rust_field_name(&contract.operations[0].name);
2996        let marker = &capability_name;
2997        match contract.operations[0].interaction.as_str() {
2998            "request" => format!(
2999                "    pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
3000            ),
3001            "stream" => format!(
3002                "    pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
3003            ),
3004            "event" => format!(
3005                "    pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
3006            ),
3007            _ => unreachable!("Descriptor validation restricts interactions"),
3008        }
3009    } else {
3010        String::new()
3011    };
3012    write!(
3013        output,
3014        "#[derive(Debug)]\npub struct {capability_name}Client {{\n{}\n}}\nimpl {capability_name}Client {{\n{}    pub fn from_dependencies(dependencies: &ModuleDependencies) -> Result<Self, RuntimeFailure> {{\n        Ok(Self {{\n{}\n        }})\n    }}\n\n{}\n}}\n\n",
3015        client_fields.join("\n"),
3016        new_method,
3017        client_initializers.join("\n"),
3018        client_methods.join("\n\n")
3019    )
3020    .expect("writing to a String cannot fail");
3021    for error in invocation_errors {
3022        output.push_str(&error);
3023    }
3024    format!("{}\n", output.trim_end())
3025}
3026
3027fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
3028    let mut types = BTreeSet::from(["UnknownDomainError"]);
3029    for operation in &contract.operations {
3030        collect_rust_runtime_types(&operation.request, &mut types);
3031        collect_rust_runtime_types(&operation.response, &mut types);
3032        for error in &operation.domain_errors {
3033            if let Some(payload) = &error.payload {
3034                if !error.payload_required && payload.is_nullable() {
3035                    types.insert("OptionalValue");
3036                }
3037                collect_rust_runtime_types(payload, &mut types);
3038            }
3039        }
3040    }
3041    types.into_iter().collect()
3042}
3043
3044fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
3045    match ty {
3046        TypeIr::Int64 => {
3047            types.insert("Int64");
3048        }
3049        TypeIr::Uint64 => {
3050            types.insert("Uint64");
3051        }
3052        TypeIr::Bytes => {
3053            types.insert("Bytes");
3054        }
3055        TypeIr::Timestamp => {
3056            types.insert("Timestamp");
3057        }
3058        TypeIr::Duration => {
3059            types.insert("Duration");
3060        }
3061        TypeIr::Array(item) | TypeIr::Nullable(item) => {
3062            collect_rust_runtime_types(item, types);
3063        }
3064        TypeIr::Object { fields, additional } => {
3065            for field in fields {
3066                if !field.required && field.ty.is_nullable() {
3067                    types.insert("OptionalValue");
3068                }
3069                collect_rust_runtime_types(&field.ty, types);
3070            }
3071            if let ObjectAdditionalIr::Typed(value) = additional {
3072                collect_rust_runtime_types(value, types);
3073            }
3074        }
3075        TypeIr::Any
3076        | TypeIr::String
3077        | TypeIr::Integer
3078        | TypeIr::Number
3079        | TypeIr::Boolean
3080        | TypeIr::Null
3081        | TypeIr::Enum(_) => {}
3082    }
3083}
3084
3085#[allow(clippy::too_many_lines)]
3086fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
3087    let mut output = String::new();
3088    writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
3089    output.push_str(
3090        "    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n",
3091    );
3092    output.push_str("        use serde::ser::SerializeMap;\n");
3093    output.push_str("        match self {\n");
3094    for variant in variants {
3095        if variant.structured {
3096            if variant.payload.is_some() {
3097                if variant.payload_required {
3098                    writeln!(
3099                        output,
3100                        "            Self::{} {{ payload }} => {{\n                let mut map = serializer.serialize_map(Some(2))?;\n                map.serialize_entry(\"code\", {})?;\n                map.serialize_entry(\"payload\", payload)?;\n                map.end()\n            }},",
3101                        variant.name,
3102                        quote_string(&variant.code)
3103                    )
3104                    .expect("String cannot fail");
3105                } else {
3106                    writeln!(
3107                        output,
3108                        "            Self::{} {{ payload }} => {{\n                let mut map = serializer.serialize_map(Some(if payload.is_some() {{ 2 }} else {{ 1 }}))?;\n                map.serialize_entry(\"code\", {})?;\n                if let Some(payload) = payload {{\n                    map.serialize_entry(\"payload\", payload)?;\n                }}\n                map.end()\n            }},",
3109                        variant.name,
3110                        quote_string(&variant.code)
3111                    )
3112                    .expect("String cannot fail");
3113                }
3114            } else {
3115                writeln!(
3116                    output,
3117                    "            Self::{} => {{\n                let mut map = serializer.serialize_map(Some(1))?;\n                map.serialize_entry(\"code\", {})?;\n                map.end()\n            }},",
3118                    variant.name,
3119                    quote_string(&variant.code)
3120                )
3121                .expect("String cannot fail");
3122            }
3123        } else {
3124            writeln!(
3125                output,
3126                "            Self::{} => serializer.serialize_str({}),",
3127                variant.name,
3128                quote_string(&variant.code)
3129            )
3130            .expect("String cannot fail");
3131        }
3132    }
3133    output.push_str("            Self::Unknown(value) => {\n                let mut map = serializer.serialize_map(Some(1 + usize::from(value.payload.is_some()) + value.extra.len()))?;\n                map.serialize_entry(\"code\", &value.code)?;\n                if let Some(payload) = &value.payload {\n                    map.serialize_entry(\"payload\", payload)?;\n                }\n                for (key, extra) in &value.extra {\n                    map.serialize_entry(key, extra)?;\n                }\n                map.end()\n            },\n        }\n    }\n}\n\n");
3134    writeln!(
3135        output,
3136        "impl<'de> serde::Deserialize<'de> for {error_name} {{"
3137    )
3138    .expect("String cannot fail");
3139    output.push_str(
3140        "    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        let value = <serde_json::Value as serde::Deserialize>::deserialize(deserializer)?;\n        match value {\n            serde_json::Value::String(code) => match code.as_str() {\n",
3141    );
3142    for variant in variants.iter().filter(|variant| !variant.structured) {
3143        writeln!(
3144            output,
3145            "                {} => Ok(Self::{}),",
3146            quote_string(&variant.code),
3147            variant.name
3148        )
3149        .expect("String cannot fail");
3150    }
3151    output.push_str(
3152        "                _ => Ok(Self::Unknown(UnknownDomainError { code, payload: None, extra: std::collections::BTreeMap::new() })),\n            },\n            serde_json::Value::Object(mut object) => {\n                let Some(code) = object.remove(\"code\").and_then(|value| value.as_str().map(ToOwned::to_owned)) else {\n                    return Err(serde::de::Error::custom(\"Domain Error object is missing a string code\"));\n                };\n",
3153    );
3154    if variants.iter().any(|variant| variant.structured) {
3155        output.push_str("                match code.as_str() {\n");
3156        for variant in variants.iter().filter(|variant| variant.structured) {
3157            if variant.payload.is_some() {
3158                if variant.payload_required {
3159                    writeln!(
3160                        output,
3161                        "                    {} => {{\n                        let payload = object.remove(\"payload\").ok_or_else(|| serde::de::Error::custom(\"structured Domain Error is missing a payload\"))?;\n                        let payload = serde_json::from_value(payload).map_err(serde::de::Error::custom)?;\n                        Ok(Self::{} {{ payload }})\n                    }},",
3162                        quote_string(&variant.code),
3163                        variant.name
3164                    )
3165                    .expect("String cannot fail");
3166                } else {
3167                    writeln!(
3168                        output,
3169                        "                    {} => {{\n                        let payload = match object.remove(\"payload\") {{\n                            Some(payload) => Some(serde_json::from_value(payload).map_err(serde::de::Error::custom)?),\n                            None => None,\n                        }};\n                        Ok(Self::{} {{ payload }})\n                    }},",
3170                        quote_string(&variant.code),
3171                        variant.name
3172                    )
3173                    .expect("String cannot fail");
3174                }
3175            } else {
3176                writeln!(
3177                    output,
3178                    "                    {} => Ok(Self::{}),",
3179                    quote_string(&variant.code),
3180                    variant.name
3181                )
3182                .expect("String cannot fail");
3183            }
3184        }
3185        output.push_str(
3186            "                    _ => {\n                        let payload = object.remove(\"payload\");\n                        let extra = object.into_iter().collect::<std::collections::BTreeMap<_, _>>();\n                        Ok(Self::Unknown(UnknownDomainError { code, payload, extra }))\n                    }\n                }\n            }\n",
3187        );
3188    } else {
3189        output.push_str(
3190            "                let payload = object.remove(\"payload\");\n                let extra = object.into_iter().collect::<std::collections::BTreeMap<_, _>>();\n                Ok(Self::Unknown(UnknownDomainError { code, payload, extra }))\n            }\n",
3191        );
3192    }
3193    output.push_str(
3194        "            other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n        }\n    }\n}\n",
3195    );
3196    output
3197}
3198
3199fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
3200    let capability_name = pascal_case(
3201        contract
3202            .capability_id
3203            .split('@')
3204            .next()
3205            .and_then(|identity| identity.rsplit('.').next())
3206            .unwrap_or("Capability"),
3207    );
3208    let codec_name = format!("{capability_name}JsonCodec");
3209    let mut types = RustTypes::new();
3210    let mut operations = Vec::new();
3211    let mut encode_arms = Vec::new();
3212    let mut response_arms = Vec::new();
3213    let mut error_arms = Vec::new();
3214
3215    for operation in &contract.operations {
3216        if operation.interaction != "request" {
3217            return Err(CodegenError::UnsupportedInteraction {
3218                operation: operation.name.clone(),
3219                interaction: operation.interaction.clone(),
3220            });
3221        }
3222        let operation_name = pascal_case(&operation.name);
3223        let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
3224        let response_type =
3225            types.type_for(&operation.response, &format!("{operation_name}Response"));
3226        let error_type = format!("{operation_name}Error");
3227        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
3228        operations.push(operation_const.clone());
3229        encode_arms.push(format!(
3230            "            {operation_const} => {{\n                let value = request.downcast_ref::<{request_type}>().ok_or_else(runtime_codec_protocol_failure)?;\n                serde_json::to_value(value).map_err(|_| runtime_codec_protocol_failure())\n            }}"
3231        ));
3232        response_arms.push(format!(
3233            "            {operation_const} => serde_json::from_value::<{response_type}>(value)\n                .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                .map_err(|_| runtime_codec_protocol_failure()),"
3234        ));
3235        error_arms.push(format!(
3236            "            {operation_const} => serde_json::from_value::<{error_type}>(value)\n                .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                .map_err(|_| runtime_codec_protocol_failure()),"
3237        ));
3238    }
3239
3240    let mut output = generate_rust(contract);
3241    write!(
3242        output,
3243        "\n#[derive(Debug, Default)]\npub struct {codec_name};\n\nimpl lenso_runtime_codec::JsonCapabilityCodec for {codec_name} {{\n    fn capability_id(&self) -> &'static str {{ CAPABILITY_ID }}\n\n    fn descriptor_version(&self) -> &'static str {{ DESCRIPTOR_VERSION }}\n\n    fn request_operations(&self) -> &'static [&'static str] {{\n        &[{}]\n    }}\n\n    fn encode_request(\n        &self,\n        operation: &str,\n        request: &dyn std::any::Any,\n    ) -> Result<serde_json::Value, RuntimeFailure> {{\n        match operation {{\n{},\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}\n    }}\n\n    fn decode_response(\n        &self,\n        operation: &str,\n        value: serde_json::Value,\n    ) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n        match operation {{\n{}\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}\n    }}\n\n    fn decode_domain_error(\n        &self,\n        operation: &str,\n        value: serde_json::Value,\n    ) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n        match operation {{\n{}\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}\n    }}\n}}\n\nfn runtime_codec_protocol_failure() -> RuntimeFailure {{\n    RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }}\n}}\n\nfn runtime_codec_unknown_operation(operation: &str) -> RuntimeFailure {{\n    RuntimeFailure::UnknownOperation {{\n        capability: CAPABILITY_ID,\n        operation: operation.to_owned(),\n    }}\n}}\n",
3244        operations.join(", "),
3245        encode_arms.join(",\n"),
3246        response_arms.join("\n"),
3247        error_arms.join("\n"),
3248    )
3249    .expect("writing generated Rust to a String cannot fail");
3250    Ok(output)
3251}
3252
3253fn generate_rust_wire_codecs(
3254    operation: &str,
3255    request_type: &str,
3256    response_type: &str,
3257    error_type: &str,
3258) -> String {
3259    let stem = snake_case(operation);
3260    format!(
3261        "pub fn encode_{stem}_request(value: &{request_type}) -> Result<String, serde_json::Error> {{ encode_portable_json(value) }}\npub fn decode_{stem}_request(wire: &str) -> Result<{request_type}, serde_json::Error> {{ decode_portable_json(wire) }}\npub fn encode_{stem}_response(value: &{response_type}) -> Result<String, serde_json::Error> {{ encode_portable_json(value) }}\npub fn decode_{stem}_response(wire: &str) -> Result<{response_type}, serde_json::Error> {{ decode_portable_json(wire) }}\npub fn encode_{stem}_error(value: &{error_type}) -> Result<String, serde_json::Error> {{ encode_portable_json(value) }}\npub fn decode_{stem}_error(wire: &str) -> Result<{error_type}, serde_json::Error> {{ decode_portable_json(wire) }}\n"
3262    )
3263}
3264
3265fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
3266    let stem = snake_case(operation);
3267    format!(
3268        "pub fn encode_{stem}_event(value: &{event_type}) -> Result<String, serde_json::Error> {{ encode_portable_json(value) }}\npub fn decode_{stem}_event(wire: &str) -> Result<{event_type}, serde_json::Error> {{ decode_portable_json(wire) }}\n"
3269    )
3270}
3271
3272fn generate_typescript_codecs(
3273    operation: &str,
3274    request_type: &str,
3275    response_type: &str,
3276    error_type: &str,
3277    variants: &[ErrorVariantIr],
3278) -> String {
3279    let stem = pascal_case(operation);
3280    let known_strings = variants
3281        .iter()
3282        .filter(|variant| !variant.structured)
3283        .map(|variant| quote_string(&variant.code))
3284        .collect::<Vec<_>>()
3285        .join(", ");
3286    format!(
3287        "export function encode{stem}Request(value: {request_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"request\"); }}\nexport function decode{stem}Request(wire: string): {request_type} {{ return lensoContractRuntime.decodePortableJson<{request_type}>(wire); }}\nexport function encode{stem}Response(value: {response_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"response\"); }}\nexport function decode{stem}Response(wire: string): {response_type} {{ return lensoContractRuntime.decodePortableJson<{response_type}>(wire); }}\nexport function encode{stem}Error(value: {error_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"Domain Error\"); }}\nexport function decode{stem}Error(wire: string): {error_type} {{ return lensoContractRuntime.decodeDomainError<{error_type}>(wire, [{known_strings}]); }}\n"
3288    )
3289}
3290
3291fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
3292    let stem = pascal_case(operation);
3293    format!(
3294        "export function encode{stem}Event(value: {event_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"event\"); }}\nexport function decode{stem}Event(wire: string): {event_type} {{ return lensoContractRuntime.decodePortableJson<{event_type}>(wire); }}\n"
3295    )
3296}
3297
3298#[allow(clippy::too_many_lines)]
3299fn generate_typescript(contract: &ContractIr) -> String {
3300    let capability_name = pascal_case(
3301        contract
3302            .capability_id
3303            .split('@')
3304            .next()
3305            .and_then(|identity| identity.rsplit('.').next())
3306            .unwrap_or("Capability"),
3307    );
3308    let mut types = TypeScriptTypes::new();
3309    let mut clients = Vec::new();
3310    let mut providers = Vec::new();
3311    let mut errors = Vec::new();
3312    let mut codecs = Vec::new();
3313    let mut request_dispatch_arms = Vec::new();
3314    let operation_names = contract
3315        .operations
3316        .iter()
3317        .map(|operation| quote_string(&operation.name))
3318        .collect::<Vec<_>>()
3319        .join(", ");
3320    let stream_operation_names = contract
3321        .operations
3322        .iter()
3323        .filter(|operation| operation.interaction == "stream")
3324        .map(|operation| quote_string(&operation.name))
3325        .collect::<Vec<_>>()
3326        .join(", ");
3327    let event_operation_names = contract
3328        .operations
3329        .iter()
3330        .filter(|operation| operation.interaction == "event")
3331        .map(|operation| quote_string(&operation.name))
3332        .collect::<Vec<_>>()
3333        .join(", ");
3334    let has_event_operations = contract
3335        .operations
3336        .iter()
3337        .any(|operation| operation.interaction == "event");
3338    for operation in &contract.operations {
3339        let operation_name = pascal_case(&operation.name);
3340        let request_name = format!("{operation_name}Request");
3341        let response_name = format!("{operation_name}Response");
3342        let error_name = format!("{operation_name}Error");
3343        let request_type = types.type_for(&operation.request, &request_name);
3344        let response_type = types.type_for(&operation.response, &response_name);
3345        let variants = &operation.domain_errors;
3346        let error_type = if variants.is_empty() {
3347            "UnknownDomainError".to_owned()
3348        } else {
3349            let mut values = variants
3350                .iter()
3351                .map(|variant| {
3352                    if let Some(payload) = &variant.payload {
3353                        let payload_name = format!("{error_name}{}Payload", variant.name);
3354                        let payload_type =
3355                            types.type_for_non_null(payload.non_null(), &payload_name);
3356                        let payload_type = if payload.is_nullable() {
3357                            format!("{payload_type} | null")
3358                        } else {
3359                            payload_type
3360                        };
3361                        let payload_optional = if variant.payload_required { "" } else { "?" };
3362                        format!(
3363                            "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
3364                            quote_string(&variant.code),
3365                        )
3366                    } else if variant.structured {
3367                        format!("{{ readonly code: {} }}", quote_string(&variant.code))
3368                    } else {
3369                        quote_string(&variant.code)
3370                    }
3371                })
3372                .collect::<Vec<_>>();
3373            values.push("UnknownDomainError".to_owned());
3374            values.join(" | ")
3375        };
3376        let invocation_error_name = format!("{operation_name}InvocationError");
3377        let result_name = format!("{operation_name}Result");
3378        let result_value_type = match operation.interaction.as_str() {
3379            "stream" => format!("StreamSession<{response_type}, {error_name}>"),
3380            "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
3381            _ => response_type.clone(),
3382        };
3383        if operation.interaction == "event" {
3384            errors.push(format!(
3385                "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
3386            ));
3387        } else {
3388            errors.push(format!(
3389                "export type {error_name} = {error_type};\nexport type {invocation_error_name} = {{ readonly kind: \"domain\"; readonly error: {error_name} }} | {{ readonly kind: \"runtime\"; readonly error: RuntimeFailure }};\nexport type {result_name} = {{ readonly ok: true; readonly value: {result_value_type} }} | {{ readonly ok: false; readonly error: {invocation_error_name} }};"
3390            ));
3391        }
3392        codecs.push(generate_typescript_codecs(
3393            &operation.name,
3394            &request_type,
3395            &response_type,
3396            &error_name,
3397            variants,
3398        ));
3399        if operation.interaction == "event" {
3400            codecs.push(generate_typescript_event_codecs(
3401                &operation.name,
3402                &request_type,
3403            ));
3404        }
3405        if matches!(operation.interaction.as_str(), "request" | "stream") {
3406            clients.push(format!(
3407                "  {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
3408                typescript_property_name(&snake_case(&operation.name)),
3409            ));
3410            providers.push(format!(
3411                "  {}(context: InvocationContext, request: {request_type}): Promise<{result_name}>;",
3412                typescript_property_name(&snake_case(&operation.name)),
3413            ));
3414        } else {
3415            clients.push(format!(
3416                "  {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
3417                typescript_property_name(&snake_case(&operation.name)),
3418            ));
3419            providers.push(format!(
3420                "  {}(context: InvocationContext, event: {request_type}): void;",
3421                typescript_property_name(&snake_case(&operation.name)),
3422            ));
3423        }
3424        if operation.interaction == "request" {
3425            let provider_method = typescript_property_name(&snake_case(&operation.name));
3426            request_dispatch_arms.push(format!(
3427                "      case {}: {{\n        let request: {request_type};\n        try {{\n          request = decode{operation_name}Request(lensoContractRuntime.encodePortableJson(payload, \"request\"));\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"protocol_violation\", detail: providerErrorMessage(error) }} }};\n        }}\n        try {{\n          const result = await provider.{provider_method}(context, request);\n          if (result.ok) {{\n            return {{ kind: \"success\", value: JSON.parse(encode{operation_name}Response(result.value)) as unknown }};\n          }}\n          if (result.error.kind === \"domain\") {{\n            return {{ kind: \"domain\", value: JSON.parse(encode{operation_name}Error(result.error.error)) as unknown }};\n          }}\n          return {{ kind: \"runtime\", failure: result.error.error }};\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"module_failure\", detail: providerErrorMessage(error) }} }};\n        }}\n      }}",
3428                quote_string(&operation.name),
3429            ));
3430        }
3431    }
3432    let mut output = String::new();
3433    output.push_str(TYPESCRIPT_HEADER);
3434    output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
3435    writeln!(
3436        output,
3437        "export const CAPABILITY_ID = {};",
3438        quote_string(&contract.capability_id)
3439    )
3440    .expect("writing to a String cannot fail");
3441    writeln!(
3442        output,
3443        "export const DESCRIPTOR_VERSION = {};",
3444        quote_string(&contract.version)
3445    )
3446    .expect("writing to a String cannot fail");
3447    writeln!(output, "export const PORTABLE = {};", contract.portable)
3448        .expect("writing to a String cannot fail");
3449    write!(
3450        output,
3451        "export const CROSS_LANE_TRANSFER = {};\n\n",
3452        contract.cross_lane_transfer
3453    )
3454    .expect("writing to a String cannot fail");
3455    output.push_str("export type Int64 = lensoContractRuntime.Int64;\nexport type Uint64 = lensoContractRuntime.Uint64;\nexport type Bytes = lensoContractRuntime.Bytes;\nexport type Timestamp = lensoContractRuntime.Timestamp;\nexport type Duration = lensoContractRuntime.Duration;\nexport type OptionalValue<T> = lensoContractRuntime.OptionalValue<T>;\nexport type InvocationContext = lensoContractRuntime.InvocationContext;\nexport type RuntimeFailure = lensoContractRuntime.RuntimeFailure;\nexport type UnknownDomainError = lensoContractRuntime.UnknownDomainError;\nexport type StreamEvent<Message, DomainError> = lensoContractRuntime.StreamEvent<Message, DomainError>;\nexport type StreamSession<Message, DomainError> = lensoContractRuntime.StreamSession<Message, DomainError>;\n\n");
3456    if has_event_operations {
3457        output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
3458    }
3459    for declaration in types.declarations {
3460        output.push_str(&declaration);
3461        output.push('\n');
3462    }
3463    for error in errors {
3464        output.push_str(&error);
3465        output.push('\n');
3466    }
3467    for codec in codecs {
3468        output.push_str(&codec);
3469        output.push('\n');
3470    }
3471    write!(
3472        output,
3473        "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
3474        clients.join("\n"),
3475        providers.join("\n")
3476    )
3477    .expect("writing to a String cannot fail");
3478    write!(
3479        output,
3480        "\nexport type ProviderDispatchOutcome =\n  | {{ readonly kind: \"success\"; readonly value: unknown }}\n  | {{ readonly kind: \"domain\"; readonly value: unknown }}\n  | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\n\nexport interface CapabilityProviderDescriptor {{\n  readonly capability_id: string;\n  readonly descriptor_version: string;\n  readonly operations: ReadonlyArray<string>;\n  readonly stream_operations: ReadonlyArray<string>;\n  readonly event_operations: ReadonlyArray<string>;\n}}\n\nexport interface CapabilityProviderBinding {{\n  readonly descriptor: CapabilityProviderDescriptor;\n  invokeRequest(\n    operation: string,\n    context: InvocationContext,\n    payload: unknown,\n  ): Promise<ProviderDispatchOutcome>;\n}}\n\nfunction providerErrorMessage(error: unknown): string {{\n  return error instanceof Error ? error.message : String(error);\n}}\n\nexport function bind{capability_name}Provider(\n  provider: {capability_name}Provider,\n): CapabilityProviderBinding {{\n  return {{\n    descriptor: {{\n      capability_id: CAPABILITY_ID,\n      descriptor_version: DESCRIPTOR_VERSION,\n      operations: [{operation_names}],\n      stream_operations: [{stream_operation_names}],\n      event_operations: [{event_operation_names}],\n    }},\n    async invokeRequest(operation, context, payload) {{\n      switch (operation) {{\n{}\n        default:\n          return {{ kind: \"runtime\", failure: {{ kind: \"unknown_operation\", operation }} }};\n      }}\n    }},\n  }};\n}}\n\nexport type Provider = {capability_name}Provider;\nexport const bindProvider = bind{capability_name}Provider;\n",
3481        request_dispatch_arms.join("\n")
3482    )
3483    .expect("writing to a String cannot fail");
3484    output.push_str(
3485        "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
3486    );
3487    output
3488}