Skip to main content

lenso_contract_codegen/
lib.rs

1//! Portable Capability Descriptor validation and binding generation.
2//!
3//! A locked Descriptor and its package-local JSON Schemas are the portable
4//! cross-language authority. They may be derived from compiled source types,
5//! but every backend consumes the same checked snapshot. This crate deliberately
6//! lives above the Kernel: it checks evolution rules and emits deterministic
7//! Rust and TypeScript artifacts before an App is booted.
8
9use std::{
10    collections::{BTreeMap, BTreeSet, HashSet},
11    error::Error,
12    fmt,
13    fmt::Write as _,
14    fs,
15    path::{Path, PathBuf},
16};
17
18use regex::Regex;
19use serde_json::{Map, Value};
20use sha2::{Digest as _, Sha256};
21
22mod browser;
23mod ir;
24mod rust_debug;
25mod source;
26mod wit;
27
28pub use browser::generate_browser_request_client;
29pub use source::{check_source_snapshot, write_source_snapshot};
30
31use ir::{ContractIr, ErrorVariantIr, FieldIr, ObjectAdditionalIr, OperationIr, TypeIr};
32
33const GENERATED_HEADER: &str = "// @generated by lenso-contract-codegen; do not edit.\n";
34const TYPESCRIPT_HEADER: &str = "/* @generated by lenso-contract-codegen; do not edit. */\n";
35const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
36
37/// A parsed Descriptor `SemVer`.
38#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
39struct Version(semver::Version);
40
41impl Version {
42    fn parse(value: &str) -> Result<Self, CodegenError> {
43        semver::Version::parse(value)
44            .map(Self)
45            .map_err(|error| CodegenError::InvalidDescriptor {
46                detail: format!("Descriptor version `{value}` is not valid SemVer: {error}"),
47            })
48    }
49
50    const fn major(&self) -> u64 {
51        self.0.major
52    }
53
54    const fn minor(&self) -> u64 {
55        self.0.minor
56    }
57}
58
59fn parse_numeric_component(value: &str) -> Option<u64> {
60    (!value.is_empty() && (value == "0" || !value.starts_with('0')))
61        .then(|| value.parse().ok())
62        .flatten()
63}
64
65#[derive(Clone, Debug)]
66struct Operation {
67    name: String,
68    interaction: String,
69    request_schema: Value,
70    response_schema: Value,
71    domain_error_schema: Value,
72}
73
74fn contract_ir(descriptor: &Descriptor) -> ContractIr {
75    ContractIr {
76        capability_id: descriptor.capability_id.clone(),
77        version: descriptor.version.clone(),
78        descriptor_digest: descriptor.digest.clone(),
79        portable: descriptor.portable,
80        cross_lane_transfer: descriptor.cross_lane_transfer,
81        operations: descriptor
82            .operations
83            .iter()
84            .map(|operation| OperationIr {
85                name: operation.name.clone(),
86                interaction: operation.interaction.clone(),
87                request: type_ir_from_schema(&operation.request_schema),
88                response: type_ir_from_schema(&operation.response_schema),
89                domain_errors: error_variant_ir_definitions(&operation.domain_error_schema),
90            })
91            .collect(),
92    }
93}
94
95fn type_ir_from_schema(schema: &Value) -> TypeIr {
96    if let Some(types) = schema.get("type").and_then(Value::as_array) {
97        let non_null_types = types
98            .iter()
99            .filter(|schema_type| *schema_type != "null")
100            .cloned()
101            .collect::<Vec<_>>();
102        if non_null_types.is_empty() {
103            return TypeIr::Null;
104        }
105        if non_null_types.len() == 1 {
106            let mut narrowed = schema.as_object().cloned().unwrap_or_default();
107            narrowed.insert("type".to_owned(), non_null_types[0].clone());
108            let base = type_ir_non_null(&Value::Object(narrowed));
109            return if non_null_types.len() == types.len() {
110                base
111            } else {
112                TypeIr::Nullable(Box::new(base))
113            };
114        }
115    }
116    type_ir_non_null(schema)
117}
118
119fn type_ir_non_null(schema: &Value) -> TypeIr {
120    let Some(object) = schema.as_object() else {
121        return TypeIr::Any;
122    };
123    if let Some(values) = object.get("enum").and_then(Value::as_array) {
124        return TypeIr::Enum {
125            name: schema_declared_type_name(object),
126            values: values
127                .iter()
128                .filter_map(Value::as_str)
129                .map(ToOwned::to_owned)
130                .collect(),
131        };
132    }
133    if let Some(schema_type) = object.get("type").and_then(Value::as_str) {
134        return match schema_type {
135            "object" => {
136                let required = required_fields(schema);
137                let mut properties = object
138                    .get("properties")
139                    .and_then(Value::as_object)
140                    .into_iter()
141                    .flatten()
142                    .collect::<Vec<_>>();
143                // Projection order is intentionally independent from JSON map
144                // insertion order. Source authoring preserves declaration order
145                // for locked snapshots; language backends retain the historical
146                // canonical name order.
147                properties.sort_unstable_by_key(|(name, _)| *name);
148                let fields = properties
149                    .into_iter()
150                    .map(|(name, schema)| {
151                        let ty = type_ir_from_schema(schema);
152                        let ty = if name.ends_with("_json") && ty == TypeIr::String {
153                            TypeIr::RawJson
154                        } else {
155                            ty
156                        };
157                        FieldIr {
158                            name: name.clone(),
159                            required: required.contains(name),
160                            sensitive: schema
161                                .get("x-lenso-sensitive")
162                                .and_then(Value::as_bool)
163                                .unwrap_or(false),
164                            ty,
165                        }
166                    })
167                    .collect();
168                let additional = match object.get("additionalProperties") {
169                    Some(Value::Bool(false)) => ObjectAdditionalIr::Closed,
170                    Some(Value::Bool(true)) | None => ObjectAdditionalIr::Any,
171                    Some(schema) => {
172                        ObjectAdditionalIr::Typed(Box::new(type_ir_from_schema(schema)))
173                    }
174                };
175                TypeIr::Object {
176                    name: schema_declared_type_name(object),
177                    fields,
178                    additional,
179                }
180            }
181            "array" => object.get("items").map_or(TypeIr::Any, |items| {
182                TypeIr::Array(Box::new(type_ir_from_schema(items)))
183            }),
184            "string" => match object.get("format").and_then(Value::as_str) {
185                Some("int64") => TypeIr::Int64,
186                Some("uint64") => TypeIr::Uint64,
187                Some("byte") => TypeIr::Bytes,
188                Some("date-time") => TypeIr::Timestamp,
189                Some("duration") => TypeIr::Duration,
190                _ => TypeIr::String,
191            },
192            "integer" => TypeIr::Integer,
193            "number" => TypeIr::Number,
194            "boolean" => TypeIr::Boolean,
195            "null" => TypeIr::Null,
196            _ => TypeIr::Any,
197        };
198    }
199    TypeIr::Any
200}
201
202fn schema_declared_type_name(schema: &Map<String, Value>) -> Option<String> {
203    schema
204        .get("title")
205        .and_then(Value::as_str)
206        .filter(|title| !title.is_empty())
207        .map(pascal_case)
208}
209
210/// A validated, self-contained portable Capability Descriptor.
211#[derive(Clone, Debug)]
212pub struct Descriptor {
213    capability_id: String,
214    capability_major: u64,
215    version: String,
216    digest: String,
217    parsed_version: Version,
218    portable: bool,
219    cross_lane_transfer: bool,
220    operations: Vec<Operation>,
221}
222
223impl Descriptor {
224    /// Returns the stable Capability series identity, including its major.
225    #[must_use]
226    pub fn capability_id(&self) -> &str {
227        &self.capability_id
228    }
229
230    /// Returns the major encoded in the Capability identity.
231    #[must_use]
232    pub const fn capability_major(&self) -> u64 {
233        self.capability_major
234    }
235
236    /// Returns the exact Descriptor `SemVer` selected for generation.
237    #[must_use]
238    pub fn version(&self) -> &str {
239        &self.version
240    }
241
242    /// Returns the canonical digest of the resolved Descriptor and Schemas.
243    #[must_use]
244    pub fn descriptor_digest(&self) -> &str {
245        &self.digest
246    }
247
248    /// Returns whether the Capability is intended to cross Runtime Adapters.
249    #[must_use]
250    pub const fn portable(&self) -> bool {
251        self.portable
252    }
253
254    /// Returns whether generated native values support transfer across Execution Lanes.
255    #[must_use]
256    pub const fn cross_lane_transfer(&self) -> bool {
257        self.cross_lane_transfer
258    }
259
260    /// Returns the stable Operation names in deterministic lexical order.
261    #[must_use]
262    pub fn operation_names(&self) -> Vec<&str> {
263        self.operations
264            .iter()
265            .map(|operation| operation.name.as_str())
266            .collect()
267    }
268}
269
270/// Metadata embedded in every generated language artifact.
271#[derive(Clone, Debug, Eq, PartialEq)]
272pub struct GeneratedMetadata {
273    /// Stable `namespace.name@major` identity.
274    pub capability_id: String,
275    /// Exact Descriptor `SemVer`.
276    pub descriptor_version: String,
277    /// Canonical SHA-256 digest of the resolved Descriptor and Schemas.
278    pub descriptor_digest: String,
279    /// Whether the source Descriptor is portable.
280    pub portable: bool,
281    /// Whether generated native values may transfer across Execution Lanes.
282    pub cross_lane_transfer: bool,
283}
284
285/// One language projection emitted from a portable Capability contract.
286#[derive(Clone, Copy, Debug, Eq, PartialEq)]
287pub enum ProjectionLanguage {
288    /// Native Rust value, consumer, and provider bindings.
289    Rust,
290    /// TypeScript value, consumer, and provider bindings.
291    TypeScript,
292    /// WebAssembly Component Model WIT provider and consumer worlds.
293    Wit,
294    /// Rust bindings plus the generated byte-Adapter Capability codec.
295    RustRuntime,
296}
297
298/// One independently generated language projection.
299#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct GeneratedProjection {
301    /// Metadata embedded in the generated projection.
302    pub metadata: GeneratedMetadata,
303    /// Selected target language.
304    pub language: ProjectionLanguage,
305    /// Generated provider, client, value, and error bindings.
306    pub source: String,
307}
308
309/// Compatibility aggregate for callers that intentionally generate both projections.
310#[derive(Clone, Debug, Eq, PartialEq)]
311pub struct GeneratedArtifacts {
312    /// Metadata shared by all generated languages.
313    pub metadata: GeneratedMetadata,
314    /// Rust provider, client, value, and error bindings.
315    pub rust: String,
316    /// TypeScript provider, client, value, and error bindings.
317    pub typescript: String,
318}
319
320/// Errors produced while parsing, validating, or generating a contract.
321#[derive(Debug)]
322pub enum CodegenError {
323    /// A file could not be read.
324    Io {
325        path: PathBuf,
326        source: std::io::Error,
327    },
328    /// Descriptor or Schema data is malformed.
329    InvalidDescriptor { detail: String },
330    /// A Schema cannot be used by the portable value profile.
331    UnsupportedSchema { path: PathBuf, detail: String },
332    /// A Descriptor interaction is not supported by the generated language bindings.
333    UnsupportedInteraction {
334        operation: String,
335        interaction: String,
336    },
337    /// A portable JSON Schema shape has no exact Component Model representation.
338    UnsupportedWit { detail: String },
339    /// A JSON value would be lossy when represented by JavaScript numbers.
340    InvalidPortableValue { path: String, detail: String },
341    /// A checked-in generated artifact differs from its source.
342    GeneratedArtifactDrift { path: PathBuf },
343}
344
345impl fmt::Display for CodegenError {
346    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347        match self {
348            Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
349            Self::InvalidDescriptor { detail } => {
350                write!(formatter, "invalid Descriptor: {detail}")
351            }
352            Self::UnsupportedSchema { path, detail } => {
353                write!(formatter, "unsupported Schema {}: {detail}", path.display())
354            }
355            Self::UnsupportedInteraction {
356                operation,
357                interaction,
358            } => write!(
359                formatter,
360                "Operation `{operation}` uses unsupported interaction `{interaction}`"
361            ),
362            Self::UnsupportedWit { detail } => {
363                write!(formatter, "unsupported WIT projection: {detail}")
364            }
365            Self::InvalidPortableValue { path, detail } => {
366                write!(formatter, "invalid portable value at `{path}`: {detail}")
367            }
368            Self::GeneratedArtifactDrift { path } => {
369                write!(formatter, "generated artifact is stale: {}", path.display())
370            }
371        }
372    }
373}
374
375impl Error for CodegenError {
376    fn source(&self) -> Option<&(dyn Error + 'static)> {
377        match self {
378            Self::Io { source, .. } => Some(source),
379            _ => None,
380        }
381    }
382}
383
384/// Reasons why a Descriptor evolution is not safe for an existing Capability
385/// series.
386#[derive(Clone, Debug, Eq, PartialEq)]
387pub enum CompatibilityError {
388    /// The two files do not describe the same Capability series.
389    IdentityChanged { from: String, to: String },
390    /// The new Descriptor is not newer than the old one.
391    VersionNotAdvanced { from: String, to: String },
392    /// The change is not allowed by the old series and requires a new major.
393    BreakingChanges { changes: Vec<String> },
394}
395
396impl fmt::Display for CompatibilityError {
397    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
398        match self {
399            Self::IdentityChanged { from, to } => {
400                write!(
401                    formatter,
402                    "Capability identity changed from `{from}` to `{to}`"
403                )
404            }
405            Self::VersionNotAdvanced { from, to } => {
406                write!(
407                    formatter,
408                    "Descriptor version must advance from `{from}` to `{to}`"
409                )
410            }
411            Self::BreakingChanges { changes } => write!(
412                formatter,
413                "breaking Descriptor changes require a new major: {}",
414                changes.join("; ")
415            ),
416        }
417    }
418}
419
420impl Error for CompatibilityError {}
421
422/// Reads and validates one Descriptor, resolving package-local `$ref` files.
423#[allow(clippy::too_many_lines)]
424pub fn load_descriptor(path: &Path) -> Result<Descriptor, CodegenError> {
425    let descriptor_path = canonical_path(path)?;
426    let package_root = descriptor_path
427        .parent()
428        .unwrap_or_else(|| Path::new("."))
429        .to_path_buf();
430    let descriptor_value = read_json(&descriptor_path)?;
431    let object = descriptor_value
432        .as_object()
433        .ok_or_else(|| CodegenError::InvalidDescriptor {
434            detail: "the Descriptor root must be an object".to_owned(),
435        })?;
436
437    let capability_id = required_string(object, "id")?;
438    let (identity, identity_major) = capability_identity(&capability_id)?;
439    let version = required_string(object, "version")?;
440    let parsed_version = Version::parse(&version)?;
441    let portable = object
442        .get("portable")
443        .and_then(Value::as_bool)
444        .unwrap_or(false);
445    let cross_lane_transfer = object
446        .get("cross_lane_transfer")
447        .and_then(Value::as_bool)
448        .unwrap_or(false);
449    let operation_values = object
450        .get("operations")
451        .and_then(Value::as_array)
452        .ok_or_else(|| CodegenError::InvalidDescriptor {
453            detail: "`operations` must be an array".to_owned(),
454        })?;
455    if operation_values.is_empty() {
456        return Err(CodegenError::InvalidDescriptor {
457            detail: "a Descriptor must declare at least one Operation".to_owned(),
458        });
459    }
460
461    let mut operations = Vec::with_capacity(operation_values.len());
462    let mut operation_names = BTreeSet::new();
463    let mut generated_operation_names = BTreeSet::new();
464    let mut generated_type_names = BTreeSet::new();
465    let mut generated_client_method_names = BTreeSet::from([
466        "new".to_owned(),
467        "from_context".to_owned(),
468        "from_dependencies".to_owned(),
469    ]);
470    let capability_name = identity
471        .rsplit('.')
472        .next()
473        .map_or_else(|| "Capability".to_owned(), pascal_case);
474    for operation_value in operation_values {
475        let operation =
476            operation_value
477                .as_object()
478                .ok_or_else(|| CodegenError::InvalidDescriptor {
479                    detail: "each Operation must be an object".to_owned(),
480                })?;
481        let name = required_string(operation, "name")?;
482        if !is_identifier(&name) || !is_rust_member_name(&name) {
483            return Err(CodegenError::InvalidDescriptor {
484                detail: format!(
485                    "Operation name `{name}` must start with a letter or `_` and contain only letters, digits, `_`, or `-`"
486                ),
487            });
488        }
489        if !operation_names.insert(name.clone()) {
490            return Err(CodegenError::InvalidDescriptor {
491                detail: format!("Operation `{name}` is declared more than once"),
492            });
493        }
494        for generated_name in [rust_field_name(&name), pascal_case(&name)] {
495            if !generated_operation_names.insert(generated_name.clone()) {
496                return Err(CodegenError::InvalidDescriptor {
497                    detail: format!(
498                        "Operation name `{name}` collides after code generation as `{generated_name}`"
499                    ),
500                });
501            }
502        }
503        let operation_name = pascal_case(&name);
504        let mut generated_names = vec![
505            format!("{operation_name}Request"),
506            format!("{operation_name}Response"),
507            format!("{operation_name}Error"),
508            format!("{operation_name}InvocationError"),
509            format!("{operation_name}Result"),
510            format!("{capability_name}{operation_name}"),
511        ];
512        generated_names.push(if operation_values.len() == 1 {
513            format!("{capability_name}InvocationError")
514        } else {
515            format!("{capability_name}{operation_name}InvocationError")
516        });
517        for generated_name in generated_names {
518            if !is_generated_type_name(&generated_name)
519                || !generated_type_names.insert(generated_name.clone())
520            {
521                return Err(CodegenError::InvalidDescriptor {
522                    detail: format!(
523                        "Operation name `{name}` collides after type generation as `{generated_name}`"
524                    ),
525                });
526            }
527        }
528        let interaction = required_string(operation, "interaction")?;
529        if !matches!(interaction.as_str(), "request" | "stream" | "event") {
530            return Err(CodegenError::InvalidDescriptor {
531                detail: format!("Operation `{name}` has unsupported interaction `{interaction}`"),
532            });
533        }
534        if matches!(interaction.as_str(), "request" | "stream" | "event") {
535            let client_method_name = rust_field_name(&name);
536            for generated_name in [
537                client_method_name.clone(),
538                format!("{client_method_name}_with_context"),
539            ] {
540                if !generated_client_method_names.insert(generated_name.clone()) {
541                    return Err(CodegenError::InvalidDescriptor {
542                        detail: format!(
543                            "Operation name `{name}` collides with the generated Client API as `{generated_name}`"
544                        ),
545                    });
546                }
547            }
548        }
549        let request_schema_path =
550            schema_path(&descriptor_path, operation, "request_schema", &name)?;
551        let response_schema_path =
552            schema_path(&descriptor_path, operation, "response_schema", &name)?;
553        let domain_error_schema_path =
554            schema_path(&descriptor_path, operation, "domain_error_schema", &name)?;
555        let mut ref_stack = Vec::new();
556        let request_source = read_json(&request_schema_path)?;
557        let mut request_schema = resolve_refs(
558            &request_source,
559            &request_source,
560            &request_schema_path,
561            &package_root,
562            &mut ref_stack,
563        )?;
564        normalize_nullable_unions(&mut request_schema);
565        validate_schema_profile(&request_schema, &request_schema_path)?;
566        validate_value_generation_schema(&request_schema, &request_schema_path)?;
567        let mut ref_stack = Vec::new();
568        let response_source = read_json(&response_schema_path)?;
569        let mut response_schema = resolve_refs(
570            &response_source,
571            &response_source,
572            &response_schema_path,
573            &package_root,
574            &mut ref_stack,
575        )?;
576        normalize_nullable_unions(&mut response_schema);
577        validate_schema_profile(&response_schema, &response_schema_path)?;
578        validate_value_generation_schema(&response_schema, &response_schema_path)?;
579        let mut ref_stack = Vec::new();
580        let domain_error_source = read_json(&domain_error_schema_path)?;
581        let mut domain_error_schema = resolve_refs(
582            &domain_error_source,
583            &domain_error_source,
584            &domain_error_schema_path,
585            &package_root,
586            &mut ref_stack,
587        )?;
588        normalize_nullable_unions(&mut domain_error_schema);
589        validate_schema_profile(&domain_error_schema, &domain_error_schema_path)?;
590        validate_domain_error_schema(&domain_error_schema, &domain_error_schema_path)?;
591        operations.push(Operation {
592            name,
593            interaction,
594            request_schema,
595            response_schema,
596            domain_error_schema,
597        });
598    }
599    operations.sort_by(|left, right| left.name.cmp(&right.name));
600
601    let capability_id = format!("{identity}@{identity_major}");
602    let descriptor_digest = resolved_descriptor_digest(
603        &capability_id,
604        &version,
605        portable,
606        cross_lane_transfer,
607        &operations,
608    );
609    Ok(Descriptor {
610        capability_id,
611        capability_major: identity_major,
612        version,
613        digest: descriptor_digest,
614        parsed_version,
615        portable,
616        cross_lane_transfer,
617        operations,
618    })
619}
620
621fn resolved_descriptor_digest(
622    capability_id: &str,
623    version: &str,
624    portable: bool,
625    cross_lane_transfer: bool,
626    operations: &[Operation],
627) -> String {
628    let operations = operations
629        .iter()
630        .map(|operation| {
631            serde_json::json!({
632                "name": operation.name,
633                "interaction": operation.interaction,
634                "request_schema": operation.request_schema,
635                "response_schema": operation.response_schema,
636                "domain_error_schema": operation.domain_error_schema,
637            })
638        })
639        .collect::<Vec<_>>();
640    let value = serde_json::json!({
641        "capability_id": capability_id,
642        "version": version,
643        "portable": portable,
644        "cross_lane_transfer": cross_lane_transfer,
645        "operations": operations,
646    });
647    let canonical = canonicalize_json(&value);
648    format!("sha256:{:x}", Sha256::digest(canonical.as_bytes()))
649}
650
651fn canonicalize_json(value: &Value) -> String {
652    match value {
653        Value::Array(values) => format!(
654            "[{}]",
655            values
656                .iter()
657                .map(canonicalize_json)
658                .collect::<Vec<_>>()
659                .join(",")
660        ),
661        Value::Object(object) => {
662            let mut keys = object.keys().collect::<Vec<_>>();
663            keys.sort();
664            format!(
665                "{{{}}}",
666                keys.into_iter()
667                    .map(|key| format!(
668                        "{}:{}",
669                        serde_json::to_string(key).expect("object keys serialize"),
670                        canonicalize_json(&object[key])
671                    ))
672                    .collect::<Vec<_>>()
673                    .join(",")
674            )
675        }
676        _ => serde_json::to_string(value).expect("JSON values serialize"),
677    }
678}
679
680fn required_string(object: &Map<String, Value>, key: &str) -> Result<String, CodegenError> {
681    object
682        .get(key)
683        .and_then(Value::as_str)
684        .filter(|value| !value.is_empty())
685        .map(ToOwned::to_owned)
686        .ok_or_else(|| CodegenError::InvalidDescriptor {
687            detail: format!("`{key}` must be a non-empty string"),
688        })
689}
690
691fn capability_identity(value: &str) -> Result<(String, u64), CodegenError> {
692    let Some((identity, major)) = value.rsplit_once('@') else {
693        return Err(CodegenError::InvalidDescriptor {
694            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
695        });
696    };
697    let generated_name = identity
698        .rsplit('.')
699        .next()
700        .map(pascal_case)
701        .unwrap_or_default();
702    if identity.is_empty()
703        || identity.split('.').count() < 2
704        || !identity.split('.').all(is_identifier)
705        || !is_generated_type_name(&generated_name)
706    {
707        return Err(CodegenError::InvalidDescriptor {
708            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
709        });
710    }
711    let Some(major) = parse_numeric_component(major) else {
712        return Err(CodegenError::InvalidDescriptor {
713            detail: format!("Capability id `{value}` has an invalid major"),
714        });
715    };
716    Ok((identity.to_owned(), major))
717}
718
719fn schema_path(
720    descriptor_path: &Path,
721    operation: &Map<String, Value>,
722    key: &str,
723    operation_name: &str,
724) -> Result<PathBuf, CodegenError> {
725    let value = required_string(operation, key).map_err(|error| match error {
726        CodegenError::InvalidDescriptor { detail } => CodegenError::InvalidDescriptor {
727            detail: format!("Operation `{operation_name}`: {detail}"),
728        },
729        other => other,
730    })?;
731    let candidate = descriptor_path
732        .parent()
733        .unwrap_or_else(|| Path::new("."))
734        .join(value);
735    let canonical = canonical_path(&candidate)?;
736    let package_root = canonical_path(descriptor_path.parent().unwrap_or_else(|| Path::new(".")))?;
737    if !canonical.starts_with(&package_root) {
738        return Err(CodegenError::UnsupportedSchema {
739            path: canonical,
740            detail: "Schema paths must remain inside the Descriptor package".to_owned(),
741        });
742    }
743    Ok(canonical)
744}
745
746fn canonical_path(path: &Path) -> Result<PathBuf, CodegenError> {
747    fs::canonicalize(path).map_err(|source| CodegenError::Io {
748        path: path.to_path_buf(),
749        source,
750    })
751}
752
753fn read_json(path: &Path) -> Result<Value, CodegenError> {
754    let source = fs::read_to_string(path).map_err(|source| CodegenError::Io {
755        path: path.to_path_buf(),
756        source,
757    })?;
758    serde_json::from_str(&source).map_err(|error| CodegenError::InvalidDescriptor {
759        detail: format!("{}: {error}", path.display()),
760    })
761}
762
763fn resolve_refs(
764    value: &Value,
765    root: &Value,
766    source_path: &Path,
767    package_root: &Path,
768    stack: &mut Vec<(PathBuf, String)>,
769) -> Result<Value, CodegenError> {
770    match value {
771        Value::Object(object) => {
772            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
773                let target_value = if reference.starts_with('#') {
774                    let fragment = reference.strip_prefix('#').unwrap_or_default();
775                    let target_path = source_path.to_path_buf();
776                    let key = (target_path.clone(), reference.to_owned());
777                    if stack.contains(&key) {
778                        return Err(CodegenError::UnsupportedSchema {
779                            path: source_path.to_path_buf(),
780                            detail: format!("cyclic local Schema `$ref` `{reference}`"),
781                        });
782                    }
783                    let target = json_pointer(root, fragment).ok_or_else(|| {
784                        CodegenError::UnsupportedSchema {
785                            path: source_path.to_path_buf(),
786                            detail: format!(
787                                "local JSON Pointer `$ref` `{reference}` was not found"
788                            ),
789                        }
790                    })?;
791                    stack.push(key);
792                    let resolved = resolve_refs(target, root, source_path, package_root, stack)?;
793                    stack.pop();
794                    resolved
795                } else {
796                    let (reference_path, fragment) =
797                        reference.split_once('#').unwrap_or((reference, ""));
798                    let target_path = source_path
799                        .parent()
800                        .unwrap_or_else(|| Path::new("."))
801                        .join(reference_path);
802                    let target_path = canonical_path(&target_path)?;
803                    if !target_path.starts_with(package_root) {
804                        return Err(CodegenError::UnsupportedSchema {
805                            path: source_path.to_path_buf(),
806                            detail: format!(
807                                "external Schema `$ref` `{reference}` leaves the Descriptor package"
808                            ),
809                        });
810                    }
811                    let key = (target_path.clone(), format!("#{fragment}"));
812                    if stack.contains(&key) {
813                        return Err(CodegenError::UnsupportedSchema {
814                            path: source_path.to_path_buf(),
815                            detail: format!(
816                                "cyclic Schema `$ref` through {}",
817                                target_path.display()
818                            ),
819                        });
820                    }
821                    let target_root = read_json(&target_path)?;
822                    let target = json_pointer(&target_root, fragment).ok_or_else(|| {
823                        CodegenError::UnsupportedSchema {
824                            path: source_path.to_path_buf(),
825                            detail: format!(
826                                "external JSON Pointer `$ref` `{reference}` was not found"
827                            ),
828                        }
829                    })?;
830                    stack.push(key);
831                    let resolved =
832                        resolve_refs(target, &target_root, &target_path, package_root, stack)?;
833                    stack.pop();
834                    resolved
835                };
836                let mut merged = target_value.as_object().cloned().ok_or_else(|| {
837                    CodegenError::UnsupportedSchema {
838                        path: source_path.to_path_buf(),
839                        detail: "a `$ref` target must resolve to an object Schema".to_owned(),
840                    }
841                })?;
842                restore_reference_type_name(&mut merged, reference);
843                for (key, child) in object {
844                    if key != "$ref" {
845                        merged.insert(
846                            key.clone(),
847                            resolve_refs(child, root, source_path, package_root, stack)?,
848                        );
849                    }
850                }
851                return Ok(Value::Object(merged));
852            }
853            let mut resolved = Map::new();
854            for (key, child) in object {
855                resolved.insert(
856                    key.clone(),
857                    resolve_refs(child, root, source_path, package_root, stack)?,
858                );
859            }
860            Ok(Value::Object(resolved))
861        }
862        Value::Array(values) => values
863            .iter()
864            .map(|child| resolve_refs(child, root, source_path, package_root, stack))
865            .collect::<Result<Vec<_>, _>>()
866            .map(Value::Array),
867        _ => Ok(value.clone()),
868    }
869}
870
871fn reference_definition_name(reference: &str) -> Option<String> {
872    let (_, fragment) = reference.rsplit_once('#')?;
873    let name = fragment.strip_prefix("/$defs/")?;
874    if name.is_empty() || name.contains('/') {
875        return None;
876    }
877    Some(name.replace("~1", "/").replace("~0", "~"))
878}
879
880fn restore_reference_type_name(schema: &mut Map<String, Value>, reference: &str) {
881    if schema.get("title").is_none()
882        && let Some(name) = reference_definition_name(reference)
883    {
884        schema.insert("title".to_owned(), Value::String(name));
885    }
886}
887
888fn json_pointer<'a>(root: &'a Value, fragment: &str) -> Option<&'a Value> {
889    if fragment.is_empty() {
890        return Some(root);
891    }
892    let pointer = fragment.strip_prefix('/')?;
893    let mut current = root;
894    for token in pointer.split('/') {
895        let token = token.replace("~1", "/").replace("~0", "~");
896        current = match current {
897            Value::Object(object) => object.get(&token)?,
898            Value::Array(array) => array.get(token.parse::<usize>().ok()?)?,
899            _ => return None,
900        };
901    }
902    Some(current)
903}
904
905fn normalize_nullable_unions(value: &mut Value) {
906    match value {
907        Value::Object(object) => {
908            for child in object.values_mut() {
909                normalize_nullable_unions(child);
910            }
911            let nullable = ["anyOf", "oneOf"].into_iter().find_map(|key| {
912                let alternatives = object.get(key)?.as_array()?;
913                if alternatives.len() != 2 {
914                    return None;
915                }
916                let null_index = alternatives.iter().position(|alternative| {
917                    alternative.get("type").and_then(Value::as_str) == Some("null")
918                })?;
919                let non_null = alternatives.get(1 - null_index)?.as_object()?.clone();
920                let schema_type = non_null
921                    .get("type")
922                    .and_then(Value::as_str)
923                    .map(ToOwned::to_owned)?;
924                Some((key, schema_type, non_null))
925            });
926            if let Some((union_key, schema_type, mut normalized)) = nullable {
927                normalized.insert(
928                    "type".to_owned(),
929                    Value::Array(vec![
930                        Value::String(schema_type),
931                        Value::String("null".to_owned()),
932                    ]),
933                );
934                for (key, child) in std::mem::take(object) {
935                    if key != union_key {
936                        normalized.entry(key).or_insert(child);
937                    }
938                }
939                *object = normalized;
940            }
941        }
942        Value::Array(values) => {
943            for child in values {
944                normalize_nullable_unions(child);
945            }
946        }
947        _ => {}
948    }
949}
950
951const SUPPORTED_SCHEMA_KEYWORDS: &[&str] = &[
952    "$defs",
953    "additionalProperties",
954    "anyOf",
955    "const",
956    "else",
957    "enum",
958    "exclusiveMaximum",
959    "exclusiveMinimum",
960    "format",
961    "if",
962    "items",
963    "maxItems",
964    "maxLength",
965    "maxProperties",
966    "maximum",
967    "minItems",
968    "minLength",
969    "minProperties",
970    "minimum",
971    "oneOf",
972    "pattern",
973    "properties",
974    "propertyNames",
975    "required",
976    "then",
977    "type",
978    "uniqueItems",
979];
980
981const HARMLESS_SCHEMA_ANNOTATIONS: &[&str] = &[
982    "$anchor",
983    "$comment",
984    "$dynamicAnchor",
985    "$id",
986    "$schema",
987    "default",
988    "deprecated",
989    "description",
990    "examples",
991    "readOnly",
992    "title",
993    "writeOnly",
994    "x-lenso-max-container-items",
995    "x-lenso-max-depth",
996    "x-lenso-max-encoded-bytes",
997    "x-lenso-max-nodes",
998    "x-lenso-max-string-length",
999    "x-lenso-sensitive",
1000];
1001
1002const PORTABLE_SCHEMA_TYPES: &[&str] = &[
1003    "array", "boolean", "integer", "null", "number", "object", "string",
1004];
1005
1006const PORTABLE_STRING_FORMATS: &[&str] = &["byte", "date-time", "duration", "int64", "uint64"];
1007const MAX_PORTABLE_PATTERN_CHARS: usize = 4_096;
1008const MAX_PORTABLE_REPETITION: u32 = 10_000;
1009const PORTABLE_UNICODE_PROPERTY_ESCAPE: &str = r"\p{Letter}";
1010
1011fn validate_schema_profile(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1012    let Some(object) = schema.as_object() else {
1013        return Err(unsupported_schema(
1014            source_path,
1015            "a JSON Schema must be an object",
1016        ));
1017    };
1018
1019    for keyword in object.keys() {
1020        if !SUPPORTED_SCHEMA_KEYWORDS.contains(&keyword.as_str())
1021            && !HARMLESS_SCHEMA_ANNOTATIONS.contains(&keyword.as_str())
1022        {
1023            return Err(unsupported_schema(
1024                source_path,
1025                format!("Schema keyword `{keyword}` is outside the portable profile"),
1026            ));
1027        }
1028    }
1029
1030    validate_schema_type_keyword(object, source_path)?;
1031    validate_schema_composition_keywords(object, source_path)?;
1032    validate_schema_scalar_keywords(object, source_path)?;
1033    validate_schema_numeric_keywords(object, source_path)?;
1034    validate_schema_children(object, source_path)
1035}
1036
1037fn validate_schema_type_keyword(
1038    object: &Map<String, Value>,
1039    source_path: &Path,
1040) -> Result<(), CodegenError> {
1041    if let Some(schema_type) = object.get("type") {
1042        match schema_type {
1043            Value::String(schema_type) => validate_schema_type(schema_type, source_path)?,
1044            Value::Array(types) if !types.is_empty() => {
1045                let mut unique = BTreeSet::new();
1046                for schema_type in types {
1047                    let Some(schema_type) = schema_type.as_str() else {
1048                        return Err(unsupported_schema(
1049                            source_path,
1050                            "Schema type unions must contain strings",
1051                        ));
1052                    };
1053                    validate_schema_type(schema_type, source_path)?;
1054                    if !unique.insert(schema_type) {
1055                        return Err(unsupported_schema(
1056                            source_path,
1057                            "Schema type unions must be unique",
1058                        ));
1059                    }
1060                }
1061            }
1062            Value::Array(_) => {
1063                return Err(unsupported_schema(
1064                    source_path,
1065                    "a Schema type union cannot be empty",
1066                ));
1067            }
1068            _ => {
1069                return Err(unsupported_schema(
1070                    source_path,
1071                    "Schema `type` must be a string or array",
1072                ));
1073            }
1074        }
1075    }
1076
1077    Ok(())
1078}
1079
1080fn validate_schema_composition_keywords(
1081    object: &Map<String, Value>,
1082    source_path: &Path,
1083) -> Result<(), CodegenError> {
1084    for keyword in ["oneOf", "anyOf"] {
1085        if let Some(value) = object.get(keyword) {
1086            let Some(alternatives) = value.as_array().filter(|values| !values.is_empty()) else {
1087                return Err(unsupported_schema(
1088                    source_path,
1089                    format!("Schema `{keyword}` must be a non-empty array of Schemas"),
1090                ));
1091            };
1092            for alternative in alternatives {
1093                validate_schema_profile(alternative, source_path)?;
1094            }
1095        }
1096    }
1097
1098    Ok(())
1099}
1100
1101fn validate_schema_scalar_keywords(
1102    object: &Map<String, Value>,
1103    source_path: &Path,
1104) -> Result<(), CodegenError> {
1105    if let Some(values) = object.get("enum")
1106        && values.as_array().is_none_or(Vec::is_empty)
1107    {
1108        return Err(unsupported_schema(
1109            source_path,
1110            "Schema `enum` must be a non-empty array",
1111        ));
1112    }
1113    if let Some(required) = object.get("required")
1114        && required
1115            .as_array()
1116            .is_none_or(|values| values.iter().any(|value| !value.is_string()))
1117    {
1118        return Err(unsupported_schema(
1119            source_path,
1120            "Schema `required` must be an array of strings",
1121        ));
1122    }
1123    if let Some(pattern) = object.get("pattern") {
1124        let Some(pattern) = pattern.as_str() else {
1125            return Err(unsupported_schema(
1126                source_path,
1127                "Schema `pattern` must be a string",
1128            ));
1129        };
1130        compile_portable_pattern(pattern).map_err(|error| {
1131            unsupported_schema(
1132                source_path,
1133                format!("Schema `pattern` is outside the portable regex subset: {error}"),
1134            )
1135        })?;
1136    }
1137    if let Some(format) = object.get("format") {
1138        let Some(format) = format.as_str() else {
1139            return Err(unsupported_schema(
1140                source_path,
1141                "Schema `format` must be a string",
1142            ));
1143        };
1144        if !PORTABLE_STRING_FORMATS.contains(&format) {
1145            return Err(unsupported_schema(
1146                source_path,
1147                format!("format `{format}` is outside the portable value profile"),
1148            ));
1149        }
1150    }
1151    if object
1152        .get("uniqueItems")
1153        .is_some_and(|value| !value.is_boolean())
1154    {
1155        return Err(unsupported_schema(
1156            source_path,
1157            "Schema `uniqueItems` must be a boolean",
1158        ));
1159    }
1160
1161    Ok(())
1162}
1163
1164fn validate_schema_numeric_keywords(
1165    object: &Map<String, Value>,
1166    source_path: &Path,
1167) -> Result<(), CodegenError> {
1168    for keyword in [
1169        "minLength",
1170        "maxLength",
1171        "minItems",
1172        "maxItems",
1173        "minProperties",
1174        "maxProperties",
1175    ] {
1176        if let Some(value) = object.get(keyword)
1177            && non_negative_safe_schema_integer(value).is_none()
1178        {
1179            return Err(unsupported_schema(
1180                source_path,
1181                format!("Schema `{keyword}` must be a non-negative safe integer"),
1182            ));
1183        }
1184    }
1185    for keyword in ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"] {
1186        if let Some(value) = object.get(keyword)
1187            && value.as_f64().is_none_or(|number| !number.is_finite())
1188        {
1189            return Err(unsupported_schema(
1190                source_path,
1191                format!("Schema `{keyword}` must be a finite number"),
1192            ));
1193        }
1194    }
1195
1196    Ok(())
1197}
1198
1199fn validate_schema_children(
1200    object: &Map<String, Value>,
1201    source_path: &Path,
1202) -> Result<(), CodegenError> {
1203    if let Some(items) = object.get("items") {
1204        validate_schema_profile(items, source_path)?;
1205    }
1206    for keyword in ["properties", "$defs"] {
1207        if let Some(value) = object.get(keyword) {
1208            let Some(schemas) = value.as_object() else {
1209                return Err(unsupported_schema(
1210                    source_path,
1211                    format!("Schema `{keyword}` must be an object of Schemas"),
1212                ));
1213            };
1214            for child in schemas.values() {
1215                validate_schema_profile(child, source_path)?;
1216            }
1217        }
1218    }
1219    if let Some(additional) = object.get("additionalProperties")
1220        && !additional.is_boolean()
1221    {
1222        validate_schema_profile(additional, source_path)?;
1223    }
1224    if let Some(property_names) = object.get("propertyNames") {
1225        validate_schema_profile(property_names, source_path)?;
1226    }
1227    if object.contains_key("then") || object.contains_key("else") {
1228        let Some(condition) = object.get("if") else {
1229            return Err(unsupported_schema(
1230                source_path,
1231                "Schema `then` and `else` require an `if` Schema",
1232            ));
1233        };
1234        validate_schema_profile(condition, source_path)?;
1235    } else if let Some(condition) = object.get("if") {
1236        validate_schema_profile(condition, source_path)?;
1237    }
1238    for keyword in ["then", "else"] {
1239        if let Some(branch) = object.get(keyword) {
1240            validate_schema_profile(branch, source_path)?;
1241        }
1242    }
1243
1244    Ok(())
1245}
1246
1247fn validate_schema_type(schema_type: &str, source_path: &Path) -> Result<(), CodegenError> {
1248    if PORTABLE_SCHEMA_TYPES.contains(&schema_type) {
1249        Ok(())
1250    } else {
1251        Err(unsupported_schema(
1252            source_path,
1253            format!("Schema type `{schema_type}` is outside the portable profile"),
1254        ))
1255    }
1256}
1257
1258/// Compiles only the regular-expression syntax whose match semantics are shared
1259/// by Rust `regex` and ECMAScript Unicode-mode `RegExp` for portable Schemas.
1260/// Engine-specific shorthand classes, internal anchors, wildcard dots,
1261/// lookarounds, backreferences, inline flags, and backtracking-unsafe
1262/// repetition shapes fail closed before either runtime uses them. Fully
1263/// anchored patterns may use the complete safe subset; start-anchored prefix
1264/// patterns are limited to fixed-shape expressions.
1265fn compile_portable_pattern(pattern: &str) -> Result<Regex, String> {
1266    if !has_portable_pattern_syntax(pattern) {
1267        return Err("pattern uses syntax outside the shared Rust/ECMAScript subset".to_owned());
1268    }
1269    Regex::new(pattern).map_err(|error| error.to_string())
1270}
1271
1272#[derive(Clone, Copy, Debug, Default)]
1273#[expect(
1274    clippy::struct_excessive_bools,
1275    reason = "these are independent regex risk facts mirrored explicitly in the TypeScript gate"
1276)]
1277struct PortablePatternAtom {
1278    can_match_empty: bool,
1279    has_variable_shape: bool,
1280    is_quantified: bool,
1281    prefix_can_match_empty: bool,
1282    contains_variable_repetition: bool,
1283    contains_alternation: bool,
1284    is_group: bool,
1285}
1286
1287#[derive(Debug)]
1288#[expect(
1289    clippy::struct_excessive_bools,
1290    reason = "branch-local and group-wide regex risk facts are independent parser state"
1291)]
1292struct PortablePatternFrame {
1293    sequence_can_match_empty: bool,
1294    earlier_branch_can_match_empty: bool,
1295    contains_variable_repetition: bool,
1296    contains_alternation: bool,
1297    branch_has_variable_shape: bool,
1298    last_atom: Option<PortablePatternAtom>,
1299}
1300
1301impl Default for PortablePatternFrame {
1302    fn default() -> Self {
1303        Self {
1304            sequence_can_match_empty: true,
1305            earlier_branch_can_match_empty: false,
1306            contains_variable_repetition: false,
1307            contains_alternation: false,
1308            branch_has_variable_shape: false,
1309            last_atom: None,
1310        }
1311    }
1312}
1313
1314#[derive(Clone, Copy, Debug)]
1315struct PortableRepetition {
1316    minimum: u32,
1317    maximum: Option<u32>,
1318}
1319
1320impl PortableRepetition {
1321    fn has_variable_extent(self) -> bool {
1322        self.maximum != Some(self.minimum)
1323    }
1324}
1325
1326#[expect(
1327    clippy::too_many_lines,
1328    reason = "keeping the single-pass scanner locally isomorphic with the TypeScript gate makes parity auditable"
1329)]
1330fn has_portable_pattern_syntax(pattern: &str) -> bool {
1331    if pattern.chars().count() > MAX_PORTABLE_PATTERN_CHARS
1332        || pattern.as_bytes().first() != Some(&b'^')
1333    {
1334        return false;
1335    }
1336
1337    let bytes = pattern.as_bytes();
1338    let mut index = 0;
1339    let mut in_class = false;
1340    let mut class_start = None;
1341    let mut frames = vec![PortablePatternFrame::default()];
1342    while index < bytes.len() {
1343        if in_class {
1344            match bytes[index] {
1345                b'\\' => {
1346                    if pattern[index..].starts_with(PORTABLE_UNICODE_PROPERTY_ESCAPE) {
1347                        index += PORTABLE_UNICODE_PROPERTY_ESCAPE.len();
1348                        continue;
1349                    }
1350                    let Some(&escaped) = bytes.get(index + 1) else {
1351                        return false;
1352                    };
1353                    if !matches!(
1354                        escaped,
1355                        b'^' | b'$'
1356                            | b'\\'
1357                            | b'.'
1358                            | b'*'
1359                            | b'+'
1360                            | b'?'
1361                            | b'('
1362                            | b')'
1363                            | b'['
1364                            | b']'
1365                            | b'{'
1366                            | b'}'
1367                            | b'|'
1368                            | b'/'
1369                            | b'-'
1370                    ) {
1371                        return false;
1372                    }
1373                    index += 2;
1374                }
1375                b']' => {
1376                    let Some(start) = class_start.take() else {
1377                        return false;
1378                    };
1379                    if matches!(&pattern[start..index], "" | "^") {
1380                        return false;
1381                    }
1382                    in_class = false;
1383                    let Some(frame) = frames.last_mut() else {
1384                        return false;
1385                    };
1386                    if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1387                        return false;
1388                    }
1389                    index += 1;
1390                }
1391                b'[' => return false,
1392                operator @ (b'&' | b'-' | b'~' | b'|')
1393                    if bytes.get(index + 1) == Some(&operator) =>
1394                {
1395                    return false;
1396                }
1397                byte if byte.is_ascii_control() => return false,
1398                byte if byte.is_ascii() => index += 1,
1399                _ => {
1400                    let Some(character) = pattern[index..].chars().next() else {
1401                        return false;
1402                    };
1403                    if character.is_control() {
1404                        return false;
1405                    }
1406                    index += character.len_utf8();
1407                }
1408            }
1409            continue;
1410        }
1411
1412        match bytes[index] {
1413            b'\\' => {
1414                if pattern[index..].starts_with(PORTABLE_UNICODE_PROPERTY_ESCAPE) {
1415                    index += PORTABLE_UNICODE_PROPERTY_ESCAPE.len();
1416                } else {
1417                    let Some(&escaped) = bytes.get(index + 1) else {
1418                        return false;
1419                    };
1420                    if !matches!(
1421                        escaped,
1422                        b'^' | b'$'
1423                            | b'\\'
1424                            | b'.'
1425                            | b'*'
1426                            | b'+'
1427                            | b'?'
1428                            | b'('
1429                            | b')'
1430                            | b'['
1431                            | b']'
1432                            | b'{'
1433                            | b'}'
1434                            | b'|'
1435                            | b'/'
1436                    ) {
1437                        return false;
1438                    }
1439                    index += 2;
1440                }
1441                let Some(frame) = frames.last_mut() else {
1442                    return false;
1443                };
1444                if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1445                    return false;
1446                }
1447            }
1448            b'[' => {
1449                in_class = true;
1450                class_start = Some(index + 1);
1451                index += 1;
1452            }
1453            b'(' => {
1454                if bytes.get(index + 1) == Some(&b'?') {
1455                    if !pattern[index..].starts_with("(?:") {
1456                        return false;
1457                    }
1458                    index += 3;
1459                } else {
1460                    index += 1;
1461                }
1462                frames.push(PortablePatternFrame::default());
1463            }
1464            b')' => {
1465                if frames.len() == 1 {
1466                    return false;
1467                }
1468                let Some(group) = frames.pop() else {
1469                    return false;
1470                };
1471                let can_match_empty =
1472                    group.earlier_branch_can_match_empty || group.sequence_can_match_empty;
1473                if can_match_empty {
1474                    return false;
1475                }
1476                let atom = PortablePatternAtom {
1477                    can_match_empty,
1478                    has_variable_shape: group.contains_variable_repetition
1479                        || group.contains_alternation,
1480                    contains_variable_repetition: group.contains_variable_repetition,
1481                    contains_alternation: group.contains_alternation,
1482                    is_group: true,
1483                    ..PortablePatternAtom::default()
1484                };
1485                let Some(parent) = frames.last_mut() else {
1486                    return false;
1487                };
1488                if !push_portable_pattern_atom(parent, atom) {
1489                    return false;
1490                }
1491                index += 1;
1492            }
1493            b'|' => {
1494                if frames.len() == 1 {
1495                    return false;
1496                }
1497                let Some(frame) = frames.last_mut() else {
1498                    return false;
1499                };
1500                frame.contains_alternation = true;
1501                frame.earlier_branch_can_match_empty |= frame.sequence_can_match_empty;
1502                frame.sequence_can_match_empty = true;
1503                frame.branch_has_variable_shape = false;
1504                frame.last_atom = None;
1505                index += 1;
1506            }
1507            b'*' | b'+' | b'?' => {
1508                let repetition = match bytes[index] {
1509                    b'*' => PortableRepetition {
1510                        minimum: 0,
1511                        maximum: None,
1512                    },
1513                    b'+' => PortableRepetition {
1514                        minimum: 1,
1515                        maximum: None,
1516                    },
1517                    b'?' => PortableRepetition {
1518                        minimum: 0,
1519                        maximum: Some(1),
1520                    },
1521                    _ => unreachable!(),
1522                };
1523                let Some(frame) = frames.last_mut() else {
1524                    return false;
1525                };
1526                if !apply_portable_repetition(frame, repetition) {
1527                    return false;
1528                }
1529                index += 1;
1530            }
1531            b'{' => {
1532                let Some(close_offset) = pattern[index + 1..].find('}') else {
1533                    return false;
1534                };
1535                let close = index + 1 + close_offset;
1536                let Some(repetition) = parse_portable_repetition(&pattern[index + 1..close]) else {
1537                    return false;
1538                };
1539                let Some(frame) = frames.last_mut() else {
1540                    return false;
1541                };
1542                if !apply_portable_repetition(frame, repetition) {
1543                    return false;
1544                }
1545                index = close + 1;
1546            }
1547            b'^' if index == 0 => {
1548                let Some(frame) = frames.last_mut() else {
1549                    return false;
1550                };
1551                frame.last_atom = None;
1552                index += 1;
1553            }
1554            b'$' if is_portable_end_anchor(pattern, index) => {
1555                let Some(frame) = frames.last_mut() else {
1556                    return false;
1557                };
1558                frame.last_atom = None;
1559                index += 1;
1560            }
1561            b']' | b'}' | b'.' | b'^' | b'$' => return false,
1562            byte if byte.is_ascii_control() => return false,
1563            byte if byte.is_ascii() => {
1564                let Some(frame) = frames.last_mut() else {
1565                    return false;
1566                };
1567                if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1568                    return false;
1569                }
1570                index += 1;
1571            }
1572            _ => {
1573                let Some(character) = pattern[index..].chars().next() else {
1574                    return false;
1575                };
1576                if character.is_control() {
1577                    return false;
1578                }
1579                let Some(frame) = frames.last_mut() else {
1580                    return false;
1581                };
1582                if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1583                    return false;
1584                }
1585                index += character.len_utf8();
1586            }
1587        }
1588    }
1589
1590    if in_class || frames.len() != 1 {
1591        return false;
1592    }
1593    let frame = &frames[0];
1594    has_portable_end_anchor(pattern)
1595        || (frame.last_atom.is_some()
1596            && !frame.contains_variable_repetition
1597            && !frame.contains_alternation)
1598}
1599
1600fn has_portable_end_anchor(pattern: &str) -> bool {
1601    let bytes = pattern.as_bytes();
1602    if bytes.last() != Some(&b'$') {
1603        return false;
1604    }
1605    bytes[..bytes.len() - 1]
1606        .iter()
1607        .rev()
1608        .take_while(|byte| **byte == b'\\')
1609        .count()
1610        % 2
1611        == 0
1612}
1613
1614fn is_portable_end_anchor(pattern: &str, index: usize) -> bool {
1615    index + 1 == pattern.len() && has_portable_end_anchor(pattern)
1616}
1617
1618fn push_portable_pattern_atom(
1619    frame: &mut PortablePatternFrame,
1620    mut atom: PortablePatternAtom,
1621) -> bool {
1622    if atom.has_variable_shape && frame.branch_has_variable_shape {
1623        return false;
1624    }
1625    atom.prefix_can_match_empty = frame.sequence_can_match_empty;
1626    frame.sequence_can_match_empty &= atom.can_match_empty;
1627    frame.branch_has_variable_shape |= atom.has_variable_shape;
1628    frame.contains_variable_repetition |= atom.contains_variable_repetition;
1629    frame.contains_alternation |= atom.contains_alternation;
1630    frame.last_atom = Some(atom);
1631    true
1632}
1633
1634fn apply_portable_repetition(
1635    frame: &mut PortablePatternFrame,
1636    repetition: PortableRepetition,
1637) -> bool {
1638    let Some(atom) = frame.last_atom.as_mut() else {
1639        return false;
1640    };
1641    if atom.is_quantified || repetition.maximum == Some(0) {
1642        return false;
1643    }
1644    if atom.is_group
1645        && (atom.can_match_empty || atom.contains_variable_repetition || atom.contains_alternation)
1646    {
1647        return false;
1648    }
1649
1650    let has_variable_extent = repetition.has_variable_extent();
1651    if has_variable_extent && frame.branch_has_variable_shape {
1652        return false;
1653    }
1654    atom.is_quantified = true;
1655    atom.has_variable_shape |= has_variable_extent;
1656    atom.can_match_empty = repetition.minimum == 0 || atom.can_match_empty;
1657    frame.sequence_can_match_empty = atom.prefix_can_match_empty && atom.can_match_empty;
1658    frame.branch_has_variable_shape |= has_variable_extent;
1659    frame.contains_variable_repetition |= has_variable_extent;
1660    true
1661}
1662
1663fn parse_portable_repetition(value: &str) -> Option<PortableRepetition> {
1664    let (minimum, maximum) = if let Some((minimum, maximum)) = value.split_once(',') {
1665        if maximum.contains(',') {
1666            return None;
1667        }
1668        let minimum = parse_portable_repetition_bound(minimum)?;
1669        let maximum = if maximum.is_empty() {
1670            None
1671        } else {
1672            Some(parse_portable_repetition_bound(maximum)?)
1673        };
1674        (minimum, maximum)
1675    } else {
1676        let minimum = parse_portable_repetition_bound(value)?;
1677        (minimum, Some(minimum))
1678    };
1679    if maximum.is_some_and(|maximum| maximum < minimum) {
1680        return None;
1681    }
1682    Some(PortableRepetition { minimum, maximum })
1683}
1684
1685fn parse_portable_repetition_bound(value: &str) -> Option<u32> {
1686    if value.is_empty()
1687        || value.len() > 1 && value.starts_with('0')
1688        || !value.bytes().all(|byte| byte.is_ascii_digit())
1689    {
1690        return None;
1691    }
1692    value
1693        .parse::<u32>()
1694        .ok()
1695        .filter(|value| *value <= MAX_PORTABLE_REPETITION)
1696}
1697
1698#[expect(
1699    clippy::cast_possible_truncation,
1700    clippy::cast_sign_loss,
1701    reason = "the finite, non-negative, integral, safe-integer checks make this conversion exact"
1702)]
1703fn non_negative_safe_schema_integer(value: &Value) -> Option<u64> {
1704    let number = value.as_f64()?;
1705    (number.is_finite()
1706        && number >= 0.0
1707        && number.fract() == 0.0
1708        && number <= 9_007_199_254_740_991.0)
1709        .then_some(number as u64)
1710}
1711
1712fn unsupported_schema(source_path: &Path, detail: impl Into<String>) -> CodegenError {
1713    CodegenError::UnsupportedSchema {
1714        path: source_path.to_path_buf(),
1715        detail: detail.into(),
1716    }
1717}
1718
1719#[allow(clippy::too_many_lines)]
1720fn validate_value_generation_schema(
1721    schema: &Value,
1722    source_path: &Path,
1723) -> Result<(), CodegenError> {
1724    let Some(object) = schema.as_object() else {
1725        return Err(CodegenError::UnsupportedSchema {
1726            path: source_path.to_path_buf(),
1727            detail: "generated value Schemas must be objects".to_owned(),
1728        });
1729    };
1730    if object.contains_key("oneOf") || object.contains_key("anyOf") {
1731        return Err(CodegenError::UnsupportedSchema {
1732            path: source_path.to_path_buf(),
1733            detail: "generated value Schemas do not support oneOf/anyOf unions".to_owned(),
1734        });
1735    }
1736    if object.contains_key("allOf") {
1737        return Err(CodegenError::UnsupportedSchema {
1738            path: source_path.to_path_buf(),
1739            detail: "generated value Schemas do not support allOf unions".to_owned(),
1740        });
1741    }
1742    if object.contains_key("const") {
1743        return Err(CodegenError::UnsupportedSchema {
1744            path: source_path.to_path_buf(),
1745            detail: "generated value Schemas do not support const fields".to_owned(),
1746        });
1747    }
1748    if let Some(types) = object.get("type").and_then(Value::as_array) {
1749        let non_null = types
1750            .iter()
1751            .filter(|schema_type| *schema_type != "null")
1752            .count();
1753        if non_null > 1 {
1754            return Err(CodegenError::UnsupportedSchema {
1755                path: source_path.to_path_buf(),
1756                detail: "generated value Schemas only support nullable type unions".to_owned(),
1757            });
1758        }
1759        let mut narrowed = object.clone();
1760        let Some(schema_type) = types.iter().find(|schema_type| *schema_type != "null") else {
1761            return Ok(());
1762        };
1763        narrowed.insert("type".to_owned(), schema_type.clone());
1764        return validate_value_generation_schema(&Value::Object(narrowed), source_path);
1765    }
1766    if let Some(values) = object.get("enum").and_then(Value::as_array) {
1767        if values.is_empty() || !values.iter().all(Value::is_string) {
1768            return Err(CodegenError::UnsupportedSchema {
1769                path: source_path.to_path_buf(),
1770                detail: "generated enum values must be non-empty strings".to_owned(),
1771            });
1772        }
1773        let mut enum_names = BTreeSet::new();
1774        for value in values.iter().filter_map(Value::as_str) {
1775            let name = pascal_case(value);
1776            if !is_generated_enum_variant(&name) || !enum_names.insert(name) {
1777                return Err(CodegenError::UnsupportedSchema {
1778                    path: source_path.to_path_buf(),
1779                    detail: "generated enum values collide after Rust variant generation"
1780                        .to_owned(),
1781                });
1782            }
1783        }
1784    }
1785    let Some(schema_type) = object.get("type").and_then(Value::as_str) else {
1786        if object.contains_key("enum") {
1787            return Ok(());
1788        }
1789        return Err(CodegenError::UnsupportedSchema {
1790            path: source_path.to_path_buf(),
1791            detail: "generated value Schemas need a supported type or string enum".to_owned(),
1792        });
1793    };
1794    match schema_type {
1795        "object" => {
1796            let properties = object
1797                .get("properties")
1798                .and_then(Value::as_object)
1799                .cloned()
1800                .unwrap_or_default();
1801            let mut rust_names = BTreeSet::new();
1802            let mut nested_type_names = BTreeSet::new();
1803            for (name, property) in &properties {
1804                if !rust_names.insert(rust_field_name(name)) {
1805                    return Err(CodegenError::UnsupportedSchema {
1806                        path: source_path.to_path_buf(),
1807                        detail: format!(
1808                            "object properties collide after Rust field normalization near `{name}`"
1809                        ),
1810                    });
1811                }
1812                if !nested_type_names.insert(pascal_case(name)) {
1813                    return Err(CodegenError::UnsupportedSchema {
1814                        path: source_path.to_path_buf(),
1815                        detail: format!(
1816                            "object properties collide after nested type-name generation near `{name}`"
1817                        ),
1818                    });
1819                }
1820                validate_value_generation_schema(property, source_path)?;
1821            }
1822            match object.get("additionalProperties") {
1823                Some(Value::Bool(false)) => {}
1824                Some(Value::Bool(true)) | None if !properties.is_empty() => {
1825                    return Err(CodegenError::UnsupportedSchema {
1826                        path: source_path.to_path_buf(),
1827                        detail: "open objects with declared properties would lose unknown fields"
1828                            .to_owned(),
1829                    });
1830                }
1831                Some(additional) if !additional.is_boolean() && !properties.is_empty() => {
1832                    return Err(CodegenError::UnsupportedSchema {
1833                        path: source_path.to_path_buf(),
1834                        detail: "objects cannot combine declared properties with a map payload"
1835                            .to_owned(),
1836                    });
1837                }
1838                Some(additional) if !additional.is_boolean() => {
1839                    validate_value_generation_schema(additional, source_path)?;
1840                }
1841                _ => {}
1842            }
1843        }
1844        "array" => {
1845            let items = object
1846                .get("items")
1847                .ok_or_else(|| CodegenError::UnsupportedSchema {
1848                    path: source_path.to_path_buf(),
1849                    detail: "generated array Schemas must declare items".to_owned(),
1850                })?;
1851            validate_value_generation_schema(items, source_path)?;
1852        }
1853        "string" | "integer" | "number" | "boolean" | "null" => {}
1854        _ => {
1855            return Err(CodegenError::UnsupportedSchema {
1856                path: source_path.to_path_buf(),
1857                detail: format!("Schema type `{schema_type}` cannot be generated"),
1858            });
1859        }
1860    }
1861    Ok(())
1862}
1863
1864fn validate_domain_error_schema(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1865    if schema
1866        .as_object()
1867        .is_some_and(|object| object.contains_key("allOf"))
1868    {
1869        return Err(CodegenError::UnsupportedSchema {
1870            path: source_path.to_path_buf(),
1871            detail: "Domain Error Schemas do not support allOf unions".to_owned(),
1872        });
1873    }
1874    let variants = schema
1875        .get("oneOf")
1876        .and_then(Value::as_array)
1877        .filter(|variants| !variants.is_empty())
1878        .ok_or_else(|| CodegenError::UnsupportedSchema {
1879            path: source_path.to_path_buf(),
1880            detail: "Domain Error Schemas must be a non-empty oneOf union".to_owned(),
1881        })?;
1882    let mut codes = BTreeSet::new();
1883    let mut names = BTreeSet::from(["Unknown".to_owned()]);
1884    for variant in variants {
1885        if variant
1886            .as_object()
1887            .is_some_and(|object| object.contains_key("allOf"))
1888        {
1889            return Err(CodegenError::UnsupportedSchema {
1890                path: source_path.to_path_buf(),
1891                detail: "Domain Error variants do not support allOf unions".to_owned(),
1892            });
1893        }
1894        if let Some(code) = variant.get("const").and_then(Value::as_str) {
1895            let name = pascal_case(code);
1896            if !codes.insert(code.to_owned())
1897                || !names.insert(name.clone())
1898                || !is_generated_enum_variant(&name)
1899            {
1900                return Err(CodegenError::UnsupportedSchema {
1901                    path: source_path.to_path_buf(),
1902                    detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1903                });
1904            }
1905            continue;
1906        }
1907        let object = variant
1908            .as_object()
1909            .ok_or_else(|| CodegenError::UnsupportedSchema {
1910                path: source_path.to_path_buf(),
1911                detail: "Domain Error variants must be string consts or objects".to_owned(),
1912            })?;
1913        let properties = object
1914            .get("properties")
1915            .and_then(Value::as_object)
1916            .ok_or_else(|| CodegenError::UnsupportedSchema {
1917                path: source_path.to_path_buf(),
1918                detail: "structured Domain Error variants need properties".to_owned(),
1919            })?;
1920        if object.get("additionalProperties") != Some(&Value::Bool(false))
1921            || !required_fields(variant).contains("code")
1922            || properties
1923                .keys()
1924                .any(|name| name != "code" && name != "payload")
1925        {
1926            return Err(CodegenError::UnsupportedSchema {
1927                path: source_path.to_path_buf(),
1928                detail:
1929                    "structured Domain Error objects must explicitly allow only code and payload"
1930                        .to_owned(),
1931            });
1932        }
1933        let code = object
1934            .get("properties")
1935            .and_then(Value::as_object)
1936            .and_then(|properties| properties.get("code"))
1937            .and_then(|code| code.get("const"))
1938            .and_then(Value::as_str)
1939            .ok_or_else(|| CodegenError::UnsupportedSchema {
1940                path: source_path.to_path_buf(),
1941                detail: "structured Domain Error variants need a const string code".to_owned(),
1942            })?;
1943        let name = pascal_case(code);
1944        if !codes.insert(code.to_owned())
1945            || !names.insert(name.clone())
1946            || !is_generated_enum_variant(&name)
1947        {
1948            return Err(CodegenError::UnsupportedSchema {
1949                path: source_path.to_path_buf(),
1950                detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1951            });
1952        }
1953        if let Some(payload) = object
1954            .get("properties")
1955            .and_then(Value::as_object)
1956            .and_then(|properties| properties.get("payload"))
1957        {
1958            validate_value_generation_schema(payload, source_path)?;
1959        }
1960    }
1961    Ok(())
1962}
1963
1964fn generation_input(path: &Path) -> Result<(GeneratedMetadata, ContractIr), CodegenError> {
1965    let descriptor = load_descriptor(path)?;
1966    let contract = contract_ir(&descriptor);
1967    let metadata = GeneratedMetadata {
1968        capability_id: descriptor.capability_id.clone(),
1969        descriptor_version: descriptor.version.clone(),
1970        descriptor_digest: descriptor.digest.clone(),
1971        portable: descriptor.portable,
1972        cross_lane_transfer: descriptor.cross_lane_transfer,
1973    };
1974    Ok((metadata, contract))
1975}
1976
1977/// Generates one selected language projection from a Descriptor source.
1978pub fn generate_projection(
1979    path: &Path,
1980    language: ProjectionLanguage,
1981) -> Result<GeneratedProjection, CodegenError> {
1982    let (metadata, contract) = generation_input(path)?;
1983    let source = match language {
1984        ProjectionLanguage::Rust => generate_rust(&contract),
1985        ProjectionLanguage::TypeScript => generate_typescript(&contract),
1986        ProjectionLanguage::Wit => wit::generate_wit(&contract)?,
1987        ProjectionLanguage::RustRuntime => generate_rust_runtime(&contract)?,
1988    };
1989    Ok(GeneratedProjection {
1990        metadata,
1991        language,
1992        source,
1993    })
1994}
1995
1996/// Generates both language artifacts from one Descriptor source.
1997pub fn generate(path: &Path) -> Result<GeneratedArtifacts, CodegenError> {
1998    let (metadata, contract) = generation_input(path)?;
1999    Ok(GeneratedArtifacts {
2000        metadata,
2001        rust: generate_rust(&contract),
2002        typescript: generate_typescript(&contract),
2003    })
2004}
2005
2006/// Writes generated artifacts to checked-in paths.
2007pub fn write_generated(
2008    descriptor_path: &Path,
2009    rust_path: &Path,
2010    typescript_path: &Path,
2011) -> Result<(), CodegenError> {
2012    let artifacts = generate(descriptor_path)?;
2013    write_artifact(rust_path, &artifacts.rust)?;
2014    write_artifact(typescript_path, &artifacts.typescript)?;
2015    Ok(())
2016}
2017
2018/// Writes one selected language projection to a checked-in path.
2019pub fn write_projection(
2020    descriptor_path: &Path,
2021    language: ProjectionLanguage,
2022    output_path: &Path,
2023) -> Result<(), CodegenError> {
2024    let projection = generate_projection(descriptor_path, language)?;
2025    write_artifact(output_path, &projection.source)
2026}
2027
2028/// Fails when either checked-in artifact is not exactly reproducible.
2029pub fn check_generated(
2030    descriptor_path: &Path,
2031    rust_path: &Path,
2032    typescript_path: &Path,
2033) -> Result<(), CodegenError> {
2034    let artifacts = generate(descriptor_path)?;
2035    check_artifact(rust_path, &artifacts.rust)?;
2036    check_artifact(typescript_path, &artifacts.typescript)?;
2037    Ok(())
2038}
2039
2040/// Fails when one selected language projection is not exactly reproducible.
2041pub fn check_projection(
2042    descriptor_path: &Path,
2043    language: ProjectionLanguage,
2044    output_path: &Path,
2045) -> Result<(), CodegenError> {
2046    let projection = generate_projection(descriptor_path, language)?;
2047    check_artifact(output_path, &projection.source)
2048}
2049
2050/// Round-trips one JSON value through the portable wire representation.
2051///
2052/// Raw JSON numbers are restricted to JavaScript's safe integer range. Wide
2053/// integers must therefore be represented by the Descriptor's `int64` or
2054/// `uint64` decimal-string formats, which this function preserves exactly.
2055pub fn round_trip_portable_json(value: &Value) -> Result<Value, CodegenError> {
2056    validate_portable_value(value, "$".to_owned())?;
2057    let encoded = serde_json::to_vec(value).expect("JSON values are serializable");
2058    serde_json::from_slice(&encoded).map_err(|error| CodegenError::InvalidPortableValue {
2059        path: "$".to_owned(),
2060        detail: format!("wire JSON could not be decoded: {error}"),
2061    })
2062}
2063
2064/// Validates a wire value against a package-local JSON Schema.
2065///
2066/// This is intentionally a small validator for the Descriptor's portable
2067/// profile rather than a general JSON Schema implementation. It makes the
2068/// profile's loss-sensitive mappings executable: wide integers must use their
2069/// decimal-string formats, bytes must be canonical base64, and timestamp and
2070/// duration strings must have their documented wire shapes.
2071pub fn validate_wire_value(schema_path: &Path, value: &Value) -> Result<(), CodegenError> {
2072    let schema_path = canonical_path(schema_path)?;
2073    let package_root = schema_path
2074        .parent()
2075        .unwrap_or_else(|| Path::new("."))
2076        .to_path_buf();
2077    let source = read_json(&schema_path)?;
2078    let mut ref_stack = Vec::new();
2079    let schema = resolve_refs(
2080        &source,
2081        &source,
2082        &schema_path,
2083        &package_root,
2084        &mut ref_stack,
2085    )?;
2086    validate_portable_value(value, "$".to_owned())?;
2087    validate_schema_profile(&schema, &schema_path)?;
2088    validate_wire_value_inner(&schema, value, "$", &schema_path)
2089}
2090
2091fn validate_wire_value_inner(
2092    schema: &Value,
2093    value: &Value,
2094    path: &str,
2095    source_path: &Path,
2096) -> Result<(), CodegenError> {
2097    if let Some(constant) = schema.get("const")
2098        && !portable_json_semantic_equal(value, constant)
2099    {
2100        return invalid_wire_value(path, "value does not match the Schema const", source_path);
2101    }
2102    if let Some(values) = schema.get("enum").and_then(Value::as_array)
2103        && !values
2104            .iter()
2105            .any(|candidate| portable_json_semantic_equal(candidate, value))
2106    {
2107        return invalid_wire_value(
2108            path,
2109            "value is not included in the Schema enum",
2110            source_path,
2111        );
2112    }
2113
2114    if let Some(alternatives) = schema.get("oneOf").and_then(Value::as_array) {
2115        let matches = alternatives
2116            .iter()
2117            .filter(|alternative| {
2118                validate_wire_value_inner(alternative, value, path, source_path).is_ok()
2119            })
2120            .count();
2121        if matches == 0
2122            && is_open_domain_error_schema(schema)
2123            && is_unknown_domain_error_wire(schema, value)
2124        {
2125            return validate_portable_value(value, path.to_owned());
2126        }
2127        if matches != 1 {
2128            return invalid_wire_value(
2129                path,
2130                "value must match exactly one Schema oneOf alternative",
2131                source_path,
2132            );
2133        }
2134    }
2135    if let Some(alternatives) = schema.get("anyOf").and_then(Value::as_array)
2136        && !alternatives.iter().any(|alternative| {
2137            validate_wire_value_inner(alternative, value, path, source_path).is_ok()
2138        })
2139    {
2140        return invalid_wire_value(
2141            path,
2142            "value must match at least one Schema anyOf alternative",
2143            source_path,
2144        );
2145    }
2146    if let Some(condition) = schema.get("if") {
2147        let branch = if validate_wire_value_inner(condition, value, path, source_path).is_ok() {
2148            schema.get("then")
2149        } else {
2150            schema.get("else")
2151        };
2152        if let Some(branch) = branch {
2153            validate_wire_value_inner(branch, value, path, source_path)?;
2154        }
2155    }
2156
2157    match schema.get("type") {
2158        Some(Value::String(schema_type)) => {
2159            validate_wire_type(schema, schema_type, value, path, source_path)
2160        }
2161        Some(Value::Array(types)) => {
2162            let mut errors = Vec::new();
2163            for schema_type in types {
2164                let Some(schema_type) = schema_type.as_str() else {
2165                    continue;
2166                };
2167                match validate_wire_type(schema, schema_type, value, path, source_path) {
2168                    Ok(()) => return Ok(()),
2169                    Err(error) => errors.push(error.to_string()),
2170                }
2171            }
2172            invalid_wire_value(
2173                path,
2174                &format!(
2175                    "value does not match any Schema type ({})",
2176                    errors.join("; ")
2177                ),
2178                source_path,
2179            )
2180        }
2181        Some(_) => invalid_wire_value(path, "Schema type must be a string or array", source_path),
2182        None if schema.as_object().is_some_and(|object| {
2183            object.contains_key("properties")
2184                || object.contains_key("required")
2185                || object.contains_key("additionalProperties")
2186        }) =>
2187        {
2188            validate_wire_type(schema, "object", value, path, source_path)
2189        }
2190        None if schema
2191            .as_object()
2192            .is_some_and(|object| object.contains_key("const") || object.contains_key("enum")) =>
2193        {
2194            Ok(())
2195        }
2196        None => Ok(()),
2197    }
2198}
2199
2200fn is_open_domain_error_schema(schema: &Value) -> bool {
2201    schema
2202        .get("oneOf")
2203        .and_then(Value::as_array)
2204        .is_some_and(|variants| {
2205            !variants.is_empty()
2206                && variants.iter().all(|variant| {
2207                    variant.get("const").and_then(Value::as_str).is_some()
2208                        || variant
2209                            .get("properties")
2210                            .and_then(Value::as_object)
2211                            .and_then(|properties| properties.get("code"))
2212                            .and_then(|code| code.get("const"))
2213                            .and_then(Value::as_str)
2214                            .is_some()
2215                })
2216        })
2217}
2218
2219fn is_unknown_domain_error_wire(schema: &Value, value: &Value) -> bool {
2220    let known_codes = schema
2221        .get("oneOf")
2222        .and_then(Value::as_array)
2223        .into_iter()
2224        .flatten()
2225        .filter_map(|variant| {
2226            variant.get("const").and_then(Value::as_str).or_else(|| {
2227                variant
2228                    .get("properties")
2229                    .and_then(Value::as_object)
2230                    .and_then(|properties| properties.get("code"))
2231                    .and_then(|code| code.get("const"))
2232                    .and_then(Value::as_str)
2233            })
2234        })
2235        .collect::<BTreeSet<_>>();
2236    match value {
2237        Value::String(code) => !known_codes.contains(code.as_str()),
2238        Value::Object(object) => object
2239            .get("code")
2240            .and_then(Value::as_str)
2241            .is_some_and(|code| !known_codes.contains(code)),
2242        _ => false,
2243    }
2244}
2245
2246fn validate_wire_type(
2247    schema: &Value,
2248    schema_type: &str,
2249    value: &Value,
2250    path: &str,
2251    source_path: &Path,
2252) -> Result<(), CodegenError> {
2253    match schema_type {
2254        "object" => validate_wire_object(schema, value, path, source_path),
2255        "array" => validate_wire_array(schema, value, path, source_path),
2256        "string" => validate_wire_string(schema, value, path, source_path),
2257        "integer" => {
2258            if !value.is_number()
2259                || value.as_i64().is_none() && value.as_u64().is_none()
2260                || !is_safe_json_integer(value)
2261            {
2262                return invalid_wire_value(path, "expected an integer", source_path);
2263            }
2264            validate_numeric_constraint(schema, value, path, source_path)
2265        }
2266        "number" => {
2267            if value
2268                .as_number()
2269                .is_none_or(|number| !is_safe_json_number(number))
2270            {
2271                return invalid_wire_value(path, "expected a finite number", source_path);
2272            }
2273            validate_numeric_constraint(schema, value, path, source_path)
2274        }
2275        "boolean" => {
2276            if !value.is_boolean() {
2277                return invalid_wire_value(path, "expected a boolean", source_path);
2278            }
2279            Ok(())
2280        }
2281        "null" => {
2282            if !value.is_null() {
2283                return invalid_wire_value(path, "expected null", source_path);
2284            }
2285            Ok(())
2286        }
2287        _ => invalid_wire_value(
2288            path,
2289            "Schema type is outside the portable profile",
2290            source_path,
2291        ),
2292    }
2293}
2294
2295fn is_safe_json_integer(value: &Value) -> bool {
2296    value
2297        .as_i64()
2298        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
2299        || value
2300            .as_u64()
2301            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
2302}
2303
2304fn is_safe_json_number(number: &serde_json::Number) -> bool {
2305    number
2306        .as_i64()
2307        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
2308        || number
2309            .as_u64()
2310            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
2311        || number.as_f64().is_some_and(|value| {
2312            value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
2313        })
2314}
2315
2316fn validate_wire_object(
2317    schema: &Value,
2318    value: &Value,
2319    path: &str,
2320    source_path: &Path,
2321) -> Result<(), CodegenError> {
2322    let Value::Object(properties) = value else {
2323        return invalid_wire_value(path, "expected an object", source_path);
2324    };
2325    let property_count = u64::try_from(properties.len()).unwrap_or(u64::MAX);
2326    if let Some(minimum) = schema
2327        .get("minProperties")
2328        .and_then(non_negative_safe_schema_integer)
2329        && property_count < minimum
2330    {
2331        return invalid_wire_value(
2332            path,
2333            "object has fewer properties than minProperties",
2334            source_path,
2335        );
2336    }
2337    if let Some(maximum) = schema
2338        .get("maxProperties")
2339        .and_then(non_negative_safe_schema_integer)
2340        && property_count > maximum
2341    {
2342        return invalid_wire_value(
2343            path,
2344            "object has more properties than maxProperties",
2345            source_path,
2346        );
2347    }
2348    let required = required_fields(schema);
2349    for field in required {
2350        if !properties.contains_key(&field) {
2351            return invalid_wire_value(
2352                &format!("{path}.{field}"),
2353                "required field is missing",
2354                source_path,
2355            );
2356        }
2357    }
2358    let declared = schema
2359        .get("properties")
2360        .and_then(Value::as_object)
2361        .cloned()
2362        .unwrap_or_default();
2363    for (field, field_value) in properties {
2364        if let Some(property_names) = schema.get("propertyNames") {
2365            validate_wire_value_inner(
2366                property_names,
2367                &Value::String(field.clone()),
2368                &format!("{path}.{field}"),
2369                source_path,
2370            )?;
2371        }
2372        if let Some(field_schema) = declared.get(field) {
2373            validate_wire_value_inner(
2374                field_schema,
2375                field_value,
2376                &format!("{path}.{field}"),
2377                source_path,
2378            )?;
2379            continue;
2380        }
2381        match schema.get("additionalProperties") {
2382            Some(Value::Bool(false)) => {
2383                return invalid_wire_value(
2384                    &format!("{path}.{field}"),
2385                    "additional property is not allowed",
2386                    source_path,
2387                );
2388            }
2389            Some(additional_schema) if !additional_schema.is_boolean() => {
2390                validate_wire_value_inner(
2391                    additional_schema,
2392                    field_value,
2393                    &format!("{path}.{field}"),
2394                    source_path,
2395                )?;
2396            }
2397            _ => {}
2398        }
2399    }
2400    Ok(())
2401}
2402
2403fn validate_wire_array(
2404    schema: &Value,
2405    value: &Value,
2406    path: &str,
2407    source_path: &Path,
2408) -> Result<(), CodegenError> {
2409    let Value::Array(values) = value else {
2410        return invalid_wire_value(path, "expected an array", source_path);
2411    };
2412    let length = u64::try_from(values.len()).unwrap_or(u64::MAX);
2413    if let Some(minimum) = schema
2414        .get("minItems")
2415        .and_then(non_negative_safe_schema_integer)
2416        && length < minimum
2417    {
2418        return invalid_wire_value(path, "array has fewer items than minItems", source_path);
2419    }
2420    if let Some(maximum) = schema
2421        .get("maxItems")
2422        .and_then(non_negative_safe_schema_integer)
2423        && length > maximum
2424    {
2425        return invalid_wire_value(path, "array has more items than maxItems", source_path);
2426    }
2427    if schema.get("uniqueItems") == Some(&Value::Bool(true)) {
2428        let mut fingerprints = HashSet::with_capacity(values.len());
2429        for value in values {
2430            if !fingerprints.insert(portable_json_fingerprint(value)) {
2431                return invalid_wire_value(path, "array items must be unique", source_path);
2432            }
2433        }
2434    }
2435    if let Some(items) = schema.get("items") {
2436        for (index, item) in values.iter().enumerate() {
2437            validate_wire_value_inner(items, item, &format!("{path}[{index}]"), source_path)?;
2438        }
2439    }
2440    Ok(())
2441}
2442
2443fn portable_json_fingerprint(value: &Value) -> String {
2444    let mut output = String::new();
2445    write_portable_json_fingerprint(value, &mut output);
2446    output
2447}
2448
2449fn portable_json_semantic_equal(left: &Value, right: &Value) -> bool {
2450    portable_json_fingerprint(left) == portable_json_fingerprint(right)
2451}
2452
2453fn write_portable_json_fingerprint(value: &Value, output: &mut String) {
2454    match value {
2455        Value::Null => output.push('n'),
2456        Value::Bool(value) => output.push_str(if *value { "b1" } else { "b0" }),
2457        Value::Number(value) => {
2458            let number = value
2459                .as_f64()
2460                .expect("portable JSON numbers are representable as f64");
2461            let bits = if number == 0.0 { 0 } else { number.to_bits() };
2462            write!(output, "d{bits:016x};").expect("writing to a String cannot fail");
2463        }
2464        Value::String(value) => {
2465            write!(output, "s{}:", value.len()).expect("writing to a String cannot fail");
2466            output.push_str(value);
2467        }
2468        Value::Array(values) => {
2469            write!(output, "a{}[", values.len()).expect("writing to a String cannot fail");
2470            for value in values {
2471                write_portable_json_fingerprint(value, output);
2472            }
2473            output.push(']');
2474        }
2475        Value::Object(values) => {
2476            let mut entries = values.iter().collect::<Vec<_>>();
2477            entries.sort_unstable_by_key(|(key, _)| *key);
2478            write!(output, "o{}{{", entries.len()).expect("writing to a String cannot fail");
2479            for (key, value) in entries {
2480                write!(output, "k{}:", key.len()).expect("writing to a String cannot fail");
2481                output.push_str(key);
2482                write_portable_json_fingerprint(value, output);
2483            }
2484            output.push('}');
2485        }
2486    }
2487}
2488
2489fn validate_wire_string(
2490    schema: &Value,
2491    value: &Value,
2492    path: &str,
2493    source_path: &Path,
2494) -> Result<(), CodegenError> {
2495    let Value::String(value) = value else {
2496        return invalid_wire_value(path, "expected a string", source_path);
2497    };
2498    validate_string_format(schema, value, path, source_path)?;
2499    if let Some(pattern) = schema.get("pattern").and_then(Value::as_str) {
2500        let Ok(pattern) = compile_portable_pattern(pattern) else {
2501            return invalid_wire_value(path, "Schema pattern could not be compiled", source_path);
2502        };
2503        if !pattern.is_match(value) {
2504            return invalid_wire_value(path, "string does not match pattern", source_path);
2505        }
2506    }
2507    let length = u64::try_from(value.chars().count()).unwrap_or(u64::MAX);
2508    if let Some(minimum) = schema
2509        .get("minLength")
2510        .and_then(non_negative_safe_schema_integer)
2511        && length < minimum
2512    {
2513        return invalid_wire_value(path, "string is shorter than minLength", source_path);
2514    }
2515    if let Some(maximum) = schema
2516        .get("maxLength")
2517        .and_then(non_negative_safe_schema_integer)
2518        && length > maximum
2519    {
2520        return invalid_wire_value(path, "string is longer than maxLength", source_path);
2521    }
2522    Ok(())
2523}
2524
2525fn validate_numeric_constraint(
2526    schema: &Value,
2527    value: &Value,
2528    path: &str,
2529    source_path: &Path,
2530) -> Result<(), CodegenError> {
2531    let Some(number) = value.as_f64() else {
2532        return Ok(());
2533    };
2534    if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64)
2535        && number < minimum
2536    {
2537        return invalid_wire_value(path, "number is below minimum", source_path);
2538    }
2539    if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64)
2540        && number > maximum
2541    {
2542        return invalid_wire_value(path, "number is above maximum", source_path);
2543    }
2544    if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64)
2545        && number <= minimum
2546    {
2547        return invalid_wire_value(path, "number is not above exclusiveMinimum", source_path);
2548    }
2549    if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64)
2550        && number >= maximum
2551    {
2552        return invalid_wire_value(path, "number is not below exclusiveMaximum", source_path);
2553    }
2554    Ok(())
2555}
2556
2557fn validate_string_format(
2558    schema: &Value,
2559    value: &str,
2560    path: &str,
2561    source_path: &Path,
2562) -> Result<(), CodegenError> {
2563    let Some(format) = schema.get("format").and_then(Value::as_str) else {
2564        return Ok(());
2565    };
2566    let valid = match format {
2567        "int64" => is_signed_decimal(value),
2568        "uint64" => is_unsigned_decimal(value),
2569        "byte" => is_base64(value),
2570        "date-time" => is_rfc3339(value),
2571        "duration" => is_iso8601_duration(value),
2572        _ => false,
2573    };
2574    if valid {
2575        Ok(())
2576    } else {
2577        invalid_wire_value(
2578            path,
2579            &format!("string does not match portable `{format}` format"),
2580            source_path,
2581        )
2582    }
2583}
2584
2585fn invalid_wire_value<T>(path: &str, detail: &str, _source_path: &Path) -> Result<T, CodegenError> {
2586    Err(CodegenError::InvalidPortableValue {
2587        path: path.to_owned(),
2588        detail: detail.to_owned(),
2589    })
2590}
2591
2592fn is_signed_decimal(value: &str) -> bool {
2593    let digits = value.strip_prefix('-').unwrap_or(value);
2594    !digits.is_empty()
2595        && (digits == "0" || !digits.starts_with('0'))
2596        && digits.chars().all(|character| character.is_ascii_digit())
2597        && value.parse::<i64>().is_ok()
2598}
2599
2600fn is_unsigned_decimal(value: &str) -> bool {
2601    !value.is_empty()
2602        && (value == "0" || !value.starts_with('0'))
2603        && value.chars().all(|character| character.is_ascii_digit())
2604        && value.parse::<u64>().is_ok()
2605}
2606
2607fn is_base64(value: &str) -> bool {
2608    let bytes = value.as_bytes();
2609    if bytes.is_empty() {
2610        return true;
2611    }
2612    if !bytes.len().is_multiple_of(4) {
2613        return false;
2614    }
2615    let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count();
2616    if padding > 2 || bytes[..bytes.len() - padding].contains(&b'=') {
2617        return false;
2618    }
2619    for byte in &bytes[..bytes.len() - padding] {
2620        if base64_digit(*byte).is_none() {
2621            return false;
2622        }
2623    }
2624    let last = &bytes[bytes.len() - 4..];
2625    if padding == 1 {
2626        base64_digit(last[2]).is_some_and(|digit| digit.trailing_zeros() >= 2)
2627    } else if padding == 2 {
2628        base64_digit(last[1]).is_some_and(|digit| digit.trailing_zeros() >= 4)
2629    } else {
2630        base64_digit(last[2]).is_some() && base64_digit(last[3]).is_some()
2631    }
2632}
2633
2634fn base64_digit(byte: u8) -> Option<u8> {
2635    match byte {
2636        b'A'..=b'Z' => Some(byte - b'A'),
2637        b'a'..=b'z' => Some(byte - b'a' + 26),
2638        b'0'..=b'9' => Some(byte - b'0' + 52),
2639        b'+' => Some(62),
2640        b'/' => Some(63),
2641        _ => None,
2642    }
2643}
2644
2645fn is_rfc3339(value: &str) -> bool {
2646    let bytes = value.as_bytes();
2647    if bytes.len() < 20
2648        || !matches!(bytes.get(4), Some(b'-'))
2649        || !matches!(bytes.get(7), Some(b'-'))
2650        || !matches!(bytes.get(10), Some(b'T' | b't'))
2651        || !matches!(bytes.get(13), Some(b':'))
2652        || !matches!(bytes.get(16), Some(b':'))
2653    {
2654        return false;
2655    }
2656    let Some(month) = fixed_digits(bytes, 5, 2) else {
2657        return false;
2658    };
2659    let Some(day) = fixed_digits(bytes, 8, 2) else {
2660        return false;
2661    };
2662    let Some(hour) = fixed_digits(bytes, 11, 2) else {
2663        return false;
2664    };
2665    let Some(minute) = fixed_digits(bytes, 14, 2) else {
2666        return false;
2667    };
2668    let Some(second) = fixed_digits(bytes, 17, 2) else {
2669        return false;
2670    };
2671    let Some(year) = fixed_digits(bytes, 0, 4) else {
2672        return false;
2673    };
2674    let max_day = match month {
2675        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2676        4 | 6 | 9 | 11 => 30,
2677        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
2678        2 => 28,
2679        _ => return false,
2680    };
2681    if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
2682        return false;
2683    }
2684    let mut index = 19;
2685    if bytes.get(index) == Some(&b'.') {
2686        index += 1;
2687        let start = index;
2688        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2689            index += 1;
2690        }
2691        if index == start {
2692            return false;
2693        }
2694    }
2695    match bytes.get(index) {
2696        Some(b'Z' | b'z') => index + 1 == bytes.len(),
2697        Some(b'+' | b'-') => {
2698            index += 1;
2699            let Some(offset_hour) = fixed_digits(bytes, index, 2) else {
2700                return false;
2701            };
2702            index += 2;
2703            if bytes.get(index) != Some(&b':') {
2704                return false;
2705            }
2706            index += 1;
2707            let Some(offset_minute) = fixed_digits(bytes, index, 2) else {
2708                return false;
2709            };
2710            offset_hour <= 23 && offset_minute <= 59 && index + 2 == bytes.len()
2711        }
2712        _ => false,
2713    }
2714}
2715
2716fn fixed_digits(bytes: &[u8], start: usize, length: usize) -> Option<u32> {
2717    let slice = bytes.get(start..start + length)?;
2718    slice.iter().try_fold(0_u32, |value, digit| {
2719        digit
2720            .is_ascii_digit()
2721            .then(|| value * 10 + u32::from(digit - b'0'))
2722    })
2723}
2724
2725fn is_iso8601_duration(value: &str) -> bool {
2726    let bytes = value.as_bytes();
2727    let mut index = usize::from(bytes.first() == Some(&b'-'));
2728    if bytes.get(index) != Some(&b'P') {
2729        return false;
2730    }
2731    index += 1;
2732    let mut in_time = false;
2733    let mut saw_component = false;
2734    let mut saw_time_component = false;
2735    let mut last_date_order = None;
2736    let mut last_time_order = None;
2737    let mut saw_week = false;
2738    let mut saw_other_date_unit = false;
2739    while index < bytes.len() {
2740        if bytes[index] == b'T' {
2741            if in_time || index + 1 == bytes.len() {
2742                return false;
2743            }
2744            in_time = true;
2745            index += 1;
2746            continue;
2747        }
2748        let digit_start = index;
2749        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2750            index += 1;
2751        }
2752        if index == digit_start {
2753            return false;
2754        }
2755
2756        let mut fractional = false;
2757        if bytes.get(index) == Some(&b'.') {
2758            fractional = true;
2759            index += 1;
2760            let fraction_start = index;
2761            while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2762                index += 1;
2763            }
2764            if index == fraction_start {
2765                return false;
2766            }
2767        }
2768
2769        let Some(&unit) = bytes.get(index) else {
2770            return false;
2771        };
2772        if in_time {
2773            let order = match unit {
2774                b'H' => 0,
2775                b'M' => 1,
2776                b'S' => 2,
2777                _ => return false,
2778            };
2779            if last_time_order.is_some_and(|previous| order <= previous) {
2780                return false;
2781            }
2782            last_time_order = Some(order);
2783            saw_time_component = true;
2784        } else {
2785            let order = match unit {
2786                b'Y' => 0,
2787                b'M' => 1,
2788                b'W' => 2,
2789                b'D' => 3,
2790                _ => return false,
2791            };
2792            if last_date_order.is_some_and(|previous| order <= previous) {
2793                return false;
2794            }
2795            last_date_order = Some(order);
2796            if unit == b'W' {
2797                if saw_other_date_unit || index + 1 != bytes.len() {
2798                    return false;
2799                }
2800                saw_week = true;
2801            } else {
2802                if saw_week {
2803                    return false;
2804                }
2805                saw_other_date_unit = true;
2806            }
2807        }
2808        saw_component = true;
2809        index += 1;
2810        if fractional && index != bytes.len() {
2811            return false;
2812        }
2813    }
2814    saw_component && (!in_time || saw_time_component)
2815}
2816
2817fn validate_portable_value(value: &Value, path: String) -> Result<(), CodegenError> {
2818    match value {
2819        Value::Number(number) => {
2820            if !is_safe_json_number(number) {
2821                return Err(CodegenError::InvalidPortableValue {
2822                    path,
2823                    detail: "wide integers must use an explicit decimal-string format".to_owned(),
2824                });
2825            }
2826        }
2827        Value::Array(values) => {
2828            for (index, value) in values.iter().enumerate() {
2829                validate_portable_value(value, format!("{path}[{index}]"))?;
2830            }
2831        }
2832        Value::Object(values) => {
2833            for (key, value) in values {
2834                validate_portable_value(value, format!("{path}.{key}"))?;
2835            }
2836        }
2837        Value::Null | Value::Bool(_) | Value::String(_) => {}
2838    }
2839    Ok(())
2840}
2841
2842fn write_artifact(path: &Path, contents: &str) -> Result<(), CodegenError> {
2843    if let Some(parent) = path.parent() {
2844        fs::create_dir_all(parent).map_err(|source| CodegenError::Io {
2845            path: parent.to_path_buf(),
2846            source,
2847        })?;
2848    }
2849    fs::write(path, contents).map_err(|source| CodegenError::Io {
2850        path: path.to_path_buf(),
2851        source,
2852    })
2853}
2854
2855fn check_artifact(path: &Path, expected: &str) -> Result<(), CodegenError> {
2856    let actual = fs::read_to_string(path).map_err(|source| CodegenError::Io {
2857        path: path.to_path_buf(),
2858        source,
2859    })?;
2860    if actual == expected {
2861        Ok(())
2862    } else {
2863        Err(CodegenError::GeneratedArtifactDrift {
2864            path: path.to_path_buf(),
2865        })
2866    }
2867}
2868
2869/// Checks whether `new_path` is an additive, compatible evolution of
2870/// `old_path`. `true` means the same Capability series remains compatible.
2871pub fn lint_compatibility(old_path: &Path, new_path: &Path) -> Result<bool, CompatibilityError> {
2872    let old = load_descriptor(old_path).map_err(|error| CompatibilityError::BreakingChanges {
2873        changes: vec![error.to_string()],
2874    })?;
2875    let new = load_descriptor(new_path).map_err(|error| CompatibilityError::BreakingChanges {
2876        changes: vec![error.to_string()],
2877    })?;
2878    if old.capability_id != new.capability_id {
2879        return Err(CompatibilityError::IdentityChanged {
2880            from: old.capability_id,
2881            to: new.capability_id,
2882        });
2883    }
2884    if new.parsed_version <= old.parsed_version {
2885        return Err(CompatibilityError::VersionNotAdvanced {
2886            from: old.version,
2887            to: new.version,
2888        });
2889    }
2890    if new.parsed_version.major() != old.parsed_version.major() {
2891        return Err(CompatibilityError::BreakingChanges {
2892            changes: vec!["a breaking change must use a new Capability @major identity".to_owned()],
2893        });
2894    }
2895
2896    let mut changes = Vec::new();
2897    if old.portable != new.portable {
2898        changes.push("Descriptor portability changed".to_owned());
2899    }
2900    if old.cross_lane_transfer && !new.cross_lane_transfer {
2901        changes.push("Descriptor cross-lane transfer support was removed".to_owned());
2902    }
2903    let old_operations: BTreeMap<_, _> = old
2904        .operations
2905        .iter()
2906        .map(|operation| (operation.name.as_str(), operation))
2907        .collect();
2908    let new_operations: BTreeMap<_, _> = new
2909        .operations
2910        .iter()
2911        .map(|operation| (operation.name.as_str(), operation))
2912        .collect();
2913
2914    for (name, old_operation) in &old_operations {
2915        let Some(new_operation) = new_operations.get(name) else {
2916            changes.push(format!("Operation `{name}` was removed"));
2917            continue;
2918        };
2919        if old_operation.interaction != new_operation.interaction {
2920            changes.push(format!("Operation `{name}` changed interaction"));
2921        }
2922        compare_schema(
2923            &old_operation.request_schema,
2924            &new_operation.request_schema,
2925            &format!("Operation `{name}` request"),
2926            &mut changes,
2927        );
2928        compare_schema(
2929            &old_operation.response_schema,
2930            &new_operation.response_schema,
2931            &format!("Operation `{name}` response"),
2932            &mut changes,
2933        );
2934        compare_error_schema(
2935            &old_operation.domain_error_schema,
2936            &new_operation.domain_error_schema,
2937            &format!("Operation `{name}` Domain Error"),
2938            &mut changes,
2939        );
2940    }
2941
2942    if new.parsed_version.minor() == old.parsed_version.minor() {
2943        if contract_signature(&old) != contract_signature(&new) {
2944            changes.push("a patch release changed the observable contract".to_owned());
2945        }
2946        for name in new_operations
2947            .keys()
2948            .filter(|name| !old_operations.contains_key(**name))
2949        {
2950            changes.push(format!("Operation `{name}` was added in a patch release"));
2951        }
2952    }
2953    if new.parsed_version.minor() < old.parsed_version.minor() {
2954        changes.push("Descriptor minor version moved backwards".to_owned());
2955    }
2956
2957    if changes.is_empty() {
2958        Ok(true)
2959    } else {
2960        Err(CompatibilityError::BreakingChanges { changes })
2961    }
2962}
2963
2964fn contract_signature(descriptor: &Descriptor) -> String {
2965    let operations = descriptor
2966        .operations
2967        .iter()
2968        .map(|operation| {
2969            serde_json::json!({
2970                "name": operation.name,
2971                "interaction": operation.interaction,
2972                "request": operation.request_schema,
2973                "response": operation.response_schema,
2974                "domain_error": canonical_domain_error_schema(&operation.domain_error_schema),
2975            })
2976        })
2977        .collect::<Vec<_>>();
2978    canonical_json(&Value::Array(operations))
2979}
2980
2981fn canonical_domain_error_schema(schema: &Value) -> Value {
2982    let Some(variants) = schema.get("oneOf").and_then(Value::as_array) else {
2983        return schema.clone();
2984    };
2985    let mut variants = variants.clone();
2986    variants.sort_by(|left, right| {
2987        error_variant_code(left)
2988            .unwrap_or_default()
2989            .cmp(error_variant_code(right).unwrap_or_default())
2990    });
2991    let mut schema = schema.as_object().cloned().unwrap_or_default();
2992    schema.insert("oneOf".to_owned(), Value::Array(variants));
2993    Value::Object(schema)
2994}
2995
2996fn error_variant_code(variant: &Value) -> Option<&str> {
2997    variant.get("const").and_then(Value::as_str).or_else(|| {
2998        variant
2999            .get("properties")
3000            .and_then(Value::as_object)
3001            .and_then(|properties| properties.get("code"))
3002            .and_then(|code| code.get("const"))
3003            .and_then(Value::as_str)
3004    })
3005}
3006
3007fn compare_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
3008    let old_nullable = is_nullable(old);
3009    let new_nullable = is_nullable(new);
3010    if old_nullable != new_nullable {
3011        changes.push(format!("{location} changed nullability"));
3012        return;
3013    }
3014    let old_type = schema_type_name(old);
3015    let new_type = schema_type_name(new);
3016    if old_type != new_type {
3017        changes.push(format!("{location} changed type"));
3018        return;
3019    }
3020    match old_type {
3021        Some("object") => {
3022            let old_properties = old.get("properties").and_then(Value::as_object);
3023            let new_properties = new.get("properties").and_then(Value::as_object);
3024            let (Some(old_properties), Some(new_properties)) = (old_properties, new_properties)
3025            else {
3026                if canonical_json(old) != canonical_json(new) {
3027                    changes.push(format!("{location} changed object shape"));
3028                }
3029                return;
3030            };
3031            let old_required = required_fields(old);
3032            let new_required = required_fields(new);
3033            for name in old_properties.keys() {
3034                let Some(new_schema) = new_properties.get(name) else {
3035                    changes.push(format!("{location} field `{name}` was removed"));
3036                    continue;
3037                };
3038                compare_schema(
3039                    &old_properties[name],
3040                    new_schema,
3041                    &format!("{location} field `{name}`"),
3042                    changes,
3043                );
3044            }
3045            for name in new_required.difference(&old_required) {
3046                changes.push(format!("{location} field `{name}` became required"));
3047            }
3048            for name in old_required.difference(&new_required) {
3049                changes.push(format!("{location} field `{name}` became optional"));
3050            }
3051            if old.get("additionalProperties") != new.get("additionalProperties") {
3052                changes.push(format!("{location} changed additional-properties policy"));
3053            }
3054            if schema_constraints_signature(old) != schema_constraints_signature(new) {
3055                changes.push(format!("{location} changed constraints"));
3056            }
3057        }
3058        Some("array") => {
3059            if let (Some(old_items), Some(new_items)) = (old.get("items"), new.get("items")) {
3060                compare_schema(old_items, new_items, &format!("{location} items"), changes);
3061            }
3062            if schema_constraints_signature(old) != schema_constraints_signature(new) {
3063                changes.push(format!("{location} changed constraints"));
3064            }
3065        }
3066        _ => {
3067            if canonical_json(old) != canonical_json(new) {
3068                changes.push(format!("{location} changed format or constraints"));
3069            }
3070        }
3071    }
3072}
3073
3074fn compare_error_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
3075    let old_variants = error_variant_values(old);
3076    let new_variants = error_variant_values(new);
3077    if old_variants.is_empty() || new_variants.is_empty() {
3078        if canonical_json(old) != canonical_json(new) {
3079            changes.push(format!("{location} changed shape"));
3080        }
3081        return;
3082    }
3083    for (code, schema) in old_variants {
3084        match new_variants.get(&code) {
3085            None => changes.push(format!("{location} variant `{code}` was removed")),
3086            Some(new_schema) => compare_schema(
3087                &schema,
3088                new_schema,
3089                &format!("{location} variant `{code}`"),
3090                changes,
3091            ),
3092        }
3093    }
3094}
3095
3096fn error_variant_ir_definitions(schema: &Value) -> Vec<ErrorVariantIr> {
3097    let mut variants = schema
3098        .get("oneOf")
3099        .and_then(Value::as_array)
3100        .into_iter()
3101        .flatten()
3102        .filter_map(|variant| {
3103            if let Some(code) = variant.get("const").and_then(Value::as_str) {
3104                return Some(ErrorVariantIr {
3105                    code: code.to_owned(),
3106                    name: pascal_case(code),
3107                    structured: false,
3108                    payload: None,
3109                    payload_required: false,
3110                });
3111            }
3112            let object = variant.as_object()?;
3113            let code = object
3114                .get("properties")
3115                .and_then(Value::as_object)
3116                .and_then(|properties| properties.get("code"))
3117                .and_then(|code| code.get("const"))
3118                .and_then(Value::as_str)?;
3119            Some(ErrorVariantIr {
3120                code: code.to_owned(),
3121                name: pascal_case(code),
3122                structured: true,
3123                payload: object
3124                    .get("properties")
3125                    .and_then(Value::as_object)
3126                    .and_then(|properties| properties.get("payload"))
3127                    .map(type_ir_from_schema),
3128                payload_required: required_fields(variant).contains("payload"),
3129            })
3130        })
3131        .collect::<Vec<_>>();
3132    variants.sort_by(|left, right| left.code.cmp(&right.code));
3133    variants
3134}
3135
3136fn error_variant_values(schema: &Value) -> BTreeMap<String, Value> {
3137    let variants = schema
3138        .get("oneOf")
3139        .and_then(Value::as_array)
3140        .into_iter()
3141        .flatten();
3142    variants
3143        .filter_map(|variant| {
3144            let code = if let Some(code) = variant.get("const").and_then(Value::as_str) {
3145                code.to_owned()
3146            } else {
3147                variant
3148                    .get("properties")
3149                    .and_then(Value::as_object)
3150                    .and_then(|properties| properties.get("code"))
3151                    .and_then(|code| code.get("const"))
3152                    .and_then(Value::as_str)
3153                    .map(ToOwned::to_owned)?
3154            };
3155            Some((code, variant.clone()))
3156        })
3157        .collect()
3158}
3159
3160fn required_fields(schema: &Value) -> BTreeSet<String> {
3161    schema
3162        .get("required")
3163        .and_then(Value::as_array)
3164        .into_iter()
3165        .flatten()
3166        .filter_map(Value::as_str)
3167        .map(ToOwned::to_owned)
3168        .collect()
3169}
3170
3171fn is_nullable(schema: &Value) -> bool {
3172    schema
3173        .get("type")
3174        .and_then(Value::as_array)
3175        .is_some_and(|types| types.iter().any(|value| value == "null"))
3176        || schema
3177            .get("anyOf")
3178            .and_then(Value::as_array)
3179            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
3180        || schema
3181            .get("oneOf")
3182            .and_then(Value::as_array)
3183            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
3184}
3185
3186fn is_null_schema(schema: &Value) -> bool {
3187    schema.get("type").is_some_and(|value| value == "null")
3188        || schema.get("const").is_some_and(Value::is_null)
3189}
3190
3191fn schema_type_name(schema: &Value) -> Option<&str> {
3192    schema.get("type").and_then(|value| match value {
3193        Value::String(value) => Some(value.as_str()),
3194        Value::Array(values) => values
3195            .iter()
3196            .find_map(Value::as_str)
3197            .filter(|value| *value != "null"),
3198        _ => None,
3199    })
3200}
3201
3202fn canonical_json(value: &Value) -> String {
3203    serde_json::to_string(value).expect("JSON values are serializable")
3204}
3205
3206fn schema_constraints_signature(schema: &Value) -> String {
3207    let mut schema = schema.as_object().cloned().unwrap_or_default();
3208    for key in [
3209        "type",
3210        "properties",
3211        "required",
3212        "additionalProperties",
3213        "items",
3214    ] {
3215        schema.remove(key);
3216    }
3217    canonical_json(&Value::Object(schema))
3218}
3219
3220fn is_identifier(value: &str) -> bool {
3221    let mut characters = value.chars();
3222    characters
3223        .next()
3224        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
3225        && characters
3226            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
3227}
3228
3229fn is_rust_member_name(value: &str) -> bool {
3230    !matches!(snake_case(value).as_str(), "self" | "super" | "crate")
3231}
3232
3233fn is_rust_type_identifier(value: &str) -> bool {
3234    let mut characters = value.chars();
3235    let valid = characters
3236        .next()
3237        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
3238        && characters.all(|character| character.is_ascii_alphanumeric() || character == '_');
3239    valid && value != "Self"
3240}
3241
3242fn is_generated_type_name(value: &str) -> bool {
3243    is_rust_type_identifier(value)
3244        && !matches!(
3245            value,
3246            "Self"
3247                | "String"
3248                | "Int64"
3249                | "Uint64"
3250                | "Bytes"
3251                | "Timestamp"
3252                | "Duration"
3253                | "OptionalValue"
3254                | "UnknownDomainError"
3255                | "InvocationContext"
3256                | "PluginDependencies"
3257                | "NativeRequestEndpoint"
3258                | "NativeRequestHandle"
3259                | "RequestCapability"
3260                | "RuntimeFailure"
3261                | "LocalBoxFuture"
3262        )
3263}
3264
3265fn is_generated_enum_variant(value: &str) -> bool {
3266    is_rust_type_identifier(value)
3267}
3268
3269fn pascal_case(value: &str) -> String {
3270    let mut output = String::new();
3271    for part in value.split(|character: char| !character.is_ascii_alphanumeric()) {
3272        if part.is_empty() || part.chars().all(char::is_numeric) {
3273            continue;
3274        }
3275        let mut chars = part.chars();
3276        if let Some(first) = chars.next() {
3277            output.extend(first.to_uppercase());
3278            output.push_str(chars.as_str());
3279        }
3280    }
3281    if output.is_empty() {
3282        "Value".to_owned()
3283    } else if output
3284        .chars()
3285        .next()
3286        .is_some_and(|character| character.is_ascii_digit())
3287    {
3288        format!("Value{output}")
3289    } else {
3290        output
3291    }
3292}
3293
3294fn snake_case(value: &str) -> String {
3295    let mut output = String::new();
3296    for (index, character) in value.chars().enumerate() {
3297        if character.is_ascii_alphanumeric() {
3298            if character.is_ascii_uppercase() && index != 0 {
3299                output.push('_');
3300            }
3301            output.push(character.to_ascii_lowercase());
3302        } else if !output.ends_with('_') {
3303            output.push('_');
3304        }
3305    }
3306    let output = output.trim_matches('_').to_owned();
3307    if output.is_empty() {
3308        "value".to_owned()
3309    } else {
3310        output
3311    }
3312}
3313
3314fn screaming_snake_case(value: &str) -> String {
3315    snake_case(value).to_ascii_uppercase()
3316}
3317
3318fn rust_field_name(value: &str) -> String {
3319    let name = snake_case(value);
3320    if matches!(name.as_str(), "self" | "super" | "crate") {
3321        return format!("{name}_");
3322    }
3323    if matches!(
3324        name.as_str(),
3325        "as" | "break"
3326            | "const"
3327            | "continue"
3328            | "crate"
3329            | "else"
3330            | "enum"
3331            | "extern"
3332            | "false"
3333            | "fn"
3334            | "for"
3335            | "if"
3336            | "impl"
3337            | "in"
3338            | "let"
3339            | "loop"
3340            | "match"
3341            | "mod"
3342            | "move"
3343            | "mut"
3344            | "pub"
3345            | "ref"
3346            | "return"
3347            | "self"
3348            | "Self"
3349            | "static"
3350            | "struct"
3351            | "super"
3352            | "trait"
3353            | "true"
3354            | "type"
3355            | "unsafe"
3356            | "use"
3357            | "where"
3358            | "while"
3359            | "async"
3360            | "await"
3361            | "dyn"
3362    ) {
3363        format!("r#{name}")
3364    } else {
3365        name
3366    }
3367}
3368
3369fn quote_string(value: &str) -> String {
3370    serde_json::to_string(value).expect("string is serializable")
3371}
3372
3373fn typescript_property_name(value: &str) -> String {
3374    let mut characters = value.chars();
3375    let valid_identifier = characters
3376        .next()
3377        .is_some_and(|character| character.is_ascii_alphabetic() || matches!(character, '_' | '$'))
3378        && characters
3379            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$'));
3380    if valid_identifier {
3381        value.to_owned()
3382    } else {
3383        quote_string(value)
3384    }
3385}
3386
3387struct RustTypes {
3388    declarations: Vec<String>,
3389    declared: BTreeSet<String>,
3390}
3391
3392impl RustTypes {
3393    fn new() -> Self {
3394        Self {
3395            declarations: Vec::new(),
3396            declared: BTreeSet::new(),
3397        }
3398    }
3399
3400    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3401        if !self.declared.insert(name.to_owned()) {
3402            return name.to_owned();
3403        }
3404        let placeholder = self.declarations.len();
3405        self.declarations.push(String::new());
3406        let mut rendered_fields = Vec::new();
3407        for field in fields {
3408            let type_name = self.type_for_non_null(
3409                field.ty.non_null(),
3410                &format!("{name}{}", pascal_case(&field.name)),
3411            );
3412            let field_type = if field.required {
3413                if field.ty.is_nullable() {
3414                    format!("Option<{type_name}>")
3415                } else {
3416                    type_name
3417                }
3418            } else if field.ty.is_nullable() {
3419                format!("OptionalValue<{type_name}>")
3420            } else {
3421                format!("Option<{type_name}>")
3422            };
3423            let mut attributes = vec![format!(
3424                "    #[serde(rename = {})]",
3425                quote_string(&field.name)
3426            )];
3427            if !field.required {
3428                if field.ty.is_nullable() {
3429                    attributes.push("    #[serde(default)]".to_owned());
3430                    attributes
3431                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
3432                    attributes.push(
3433                        "    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_optional_value\")]"
3434                            .to_owned(),
3435                    );
3436                } else {
3437                    attributes
3438                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
3439                }
3440            }
3441            if field.required {
3442                attributes
3443                    .push("    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_required\")]".to_owned());
3444            }
3445            rendered_fields.push(format!(
3446                "{}\n    pub {}: {field_type},",
3447                attributes.join("\n"),
3448                rust_field_name(&field.name)
3449            ));
3450        }
3451        let debug_impl = rust_debug::render(
3452            name,
3453            fields.iter().map(|field| {
3454                (
3455                    field.name.as_str(),
3456                    rust_field_name(&field.name),
3457                    field.sensitive,
3458                )
3459            }),
3460        );
3461        let derives = if debug_impl.is_some() {
3462            "Clone, PartialEq, serde::Serialize, serde::Deserialize"
3463        } else {
3464            "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
3465        };
3466        self.declarations[placeholder] = format!(
3467            "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
3468            rendered_fields.join("\n"),
3469            debug_impl = debug_impl.unwrap_or_default(),
3470        );
3471        name.to_owned()
3472    }
3473
3474    fn enum_type(&mut self, name: &str, values: &[String]) -> String {
3475        if !self.declared.insert(name.to_owned()) {
3476            return name.to_owned();
3477        }
3478        let variants = values
3479            .iter()
3480            .map(|value| {
3481                format!(
3482                    "    #[serde(rename = {})]\n    {},",
3483                    quote_string(value),
3484                    pascal_case(value)
3485                )
3486            })
3487            .collect::<Vec<_>>();
3488        self.declarations.push(format!(
3489            "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
3490            variants.join("\n")
3491        ));
3492        name.to_owned()
3493    }
3494
3495    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3496        let base = self.type_for_non_null(ty.non_null(), nested_name);
3497        if ty.is_nullable() {
3498            format!("Option<{base}>")
3499        } else {
3500            base
3501        }
3502    }
3503
3504    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3505        match ty {
3506            TypeIr::Any => "serde_json::Value".to_owned(),
3507            TypeIr::String => "String".to_owned(),
3508            TypeIr::RawJson => "RawJson".to_owned(),
3509            TypeIr::Enum { name, values } => {
3510                self.enum_type(name.as_deref().unwrap_or(nested_name), values)
3511            }
3512            TypeIr::Int64 => "Int64".to_owned(),
3513            TypeIr::Uint64 => "Uint64".to_owned(),
3514            TypeIr::Bytes => "Bytes".to_owned(),
3515            TypeIr::Timestamp => "Timestamp".to_owned(),
3516            TypeIr::Duration => "Duration".to_owned(),
3517            TypeIr::Integer => "i64".to_owned(),
3518            TypeIr::Number => "f64".to_owned(),
3519            TypeIr::Boolean => "bool".to_owned(),
3520            TypeIr::Null => "()".to_owned(),
3521            TypeIr::Array(items) => {
3522                format!(
3523                    "Vec<{}>",
3524                    self.type_for(items, &format!("{nested_name}Item"))
3525                )
3526            }
3527            TypeIr::Object {
3528                name,
3529                fields,
3530                additional,
3531            } => {
3532                let nested_name = name.as_deref().unwrap_or(nested_name);
3533                if fields.is_empty() {
3534                    match additional {
3535                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3536                        ObjectAdditionalIr::Any => {
3537                            "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
3538                        }
3539                        ObjectAdditionalIr::Typed(values) => format!(
3540                            "std::collections::BTreeMap<String, {}>",
3541                            self.type_for(values, &format!("{nested_name}Value"))
3542                        ),
3543                    }
3544                } else {
3545                    self.object(nested_name, fields)
3546                }
3547            }
3548            TypeIr::Nullable(inner) => {
3549                format!("Option<{}>", self.type_for_non_null(inner, nested_name))
3550            }
3551        }
3552    }
3553}
3554
3555struct TypeScriptTypes {
3556    declarations: Vec<String>,
3557    declared: BTreeSet<String>,
3558}
3559
3560impl TypeScriptTypes {
3561    fn new() -> Self {
3562        Self {
3563            declarations: Vec::new(),
3564            declared: BTreeSet::new(),
3565        }
3566    }
3567
3568    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3569        if !self.declared.insert(name.to_owned()) {
3570            return name.to_owned();
3571        }
3572        let placeholder = self.declarations.len();
3573        self.declarations.push(String::new());
3574        let mut rendered_fields = Vec::new();
3575        for field in fields {
3576            let type_name = self.type_for_non_null(
3577                field.ty.non_null(),
3578                &format!("{name}{}", pascal_case(&field.name)),
3579            );
3580            let optional = if field.required { "" } else { "?" };
3581            let field_type = if field.ty.is_nullable() {
3582                format!("{type_name} | null")
3583            } else {
3584                type_name
3585            };
3586            rendered_fields.push(format!(
3587                "  {}{optional}: {field_type};",
3588                typescript_property_name(&field.name)
3589            ));
3590        }
3591        self.declarations[placeholder] = format!(
3592            "export interface {name} {{\n{}\n}}\n",
3593            rendered_fields.join("\n")
3594        );
3595        name.to_owned()
3596    }
3597
3598    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3599        let base = self.type_for_non_null(ty.non_null(), nested_name);
3600        if ty.is_nullable() {
3601            format!("{base} | null")
3602        } else {
3603            base
3604        }
3605    }
3606
3607    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3608        match ty {
3609            TypeIr::Any => "unknown".to_owned(),
3610            TypeIr::String | TypeIr::RawJson => "string".to_owned(),
3611            TypeIr::Int64 => "Int64".to_owned(),
3612            TypeIr::Uint64 => "Uint64".to_owned(),
3613            TypeIr::Bytes => "Bytes".to_owned(),
3614            TypeIr::Timestamp => "Timestamp".to_owned(),
3615            TypeIr::Duration => "Duration".to_owned(),
3616            TypeIr::Integer | TypeIr::Number => "number".to_owned(),
3617            TypeIr::Boolean => "boolean".to_owned(),
3618            TypeIr::Null => "null".to_owned(),
3619            TypeIr::Enum { values, .. } => values
3620                .iter()
3621                .map(|value| quote_string(value))
3622                .collect::<Vec<_>>()
3623                .join(" | "),
3624            TypeIr::Array(items) => {
3625                format!(
3626                    "Array<{}>",
3627                    self.type_for(items, &format!("{nested_name}Item"))
3628                )
3629            }
3630            TypeIr::Object {
3631                name,
3632                fields,
3633                additional,
3634            } => {
3635                let nested_name = name.as_deref().unwrap_or(nested_name);
3636                if fields.is_empty() {
3637                    match additional {
3638                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3639                        ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
3640                        ObjectAdditionalIr::Typed(values) => format!(
3641                            "Record<string, {}>",
3642                            self.type_for(values, &format!("{nested_name}Value"))
3643                        ),
3644                    }
3645                } else {
3646                    self.object(nested_name, fields)
3647                }
3648            }
3649            TypeIr::Nullable(inner) => {
3650                format!("{} | null", self.type_for_non_null(inner, nested_name))
3651            }
3652        }
3653    }
3654}
3655
3656#[allow(clippy::too_many_lines)]
3657fn generate_rust(contract: &ContractIr) -> String {
3658    let capability_name = pascal_case(
3659        contract
3660            .capability_id
3661            .split('@')
3662            .next()
3663            .and_then(|identity| identity.rsplit('.').next())
3664            .unwrap_or("Capability"),
3665    );
3666    let capability_const = screaming_snake_case(&capability_name);
3667    let native_support_name = format!("__LensoNativeSupport{capability_name}");
3668    let mut types = RustTypes::new();
3669    let mut operation_rows = Vec::new();
3670    let mut stream_operation_rows = Vec::new();
3671    let mut event_operation_rows = Vec::new();
3672    let mut operation_markers = Vec::new();
3673    let mut provider_methods = Vec::new();
3674    let mut provider_lowering_methods = Vec::new();
3675    let mut object_provider_lowering_methods = Vec::new();
3676    let mut trait_object_provider_lowering_methods = Vec::new();
3677    let mut provider_result_conversions = Vec::new();
3678    let mut endpoint_arms = Vec::new();
3679    let mut stream_endpoint_arms = Vec::new();
3680    let mut event_endpoint_arms = Vec::new();
3681    let mut client_fields = Vec::new();
3682    let mut client_initializers = Vec::new();
3683    let mut many_client_initializers = Vec::new();
3684    let mut client_methods = Vec::new();
3685    let mut invocation_errors = Vec::new();
3686    let mut error_codecs = Vec::new();
3687    let mut wire_codecs = Vec::new();
3688
3689    for operation in &contract.operations {
3690        let operation_name = pascal_case(&operation.name);
3691        let request_name = format!("{operation_name}Request");
3692        let response_name = format!("{operation_name}Response");
3693        let error_name = format!("{operation_name}Error");
3694        let marker_name = if contract.operations.len() == 1 {
3695            capability_name.clone()
3696        } else {
3697            format!("{capability_name}{operation_name}")
3698        };
3699        let request_type = types.type_for(&operation.request, &request_name);
3700        let response_type = types.type_for(&operation.response, &response_name);
3701        let known_errors = &operation.domain_errors;
3702        let error_definition = if known_errors.is_empty() {
3703            format!(
3704                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n    Unknown(UnknownDomainError),\n}}\n"
3705            )
3706        } else {
3707            let variants = known_errors
3708                .iter()
3709                .map(|variant| {
3710                    if let Some(payload) = &variant.payload {
3711                        let payload_name = format!("{error_name}{}Payload", variant.name);
3712                        let payload_type =
3713                            types.type_for_non_null(payload.non_null(), &payload_name);
3714                        let payload_type = if variant.payload_required {
3715                            if payload.is_nullable() {
3716                                format!("Option<{payload_type}>")
3717                            } else {
3718                                payload_type
3719                            }
3720                        } else if payload.is_nullable() {
3721                            format!("OptionalValue<{payload_type}>")
3722                        } else {
3723                            format!("Option<{payload_type}>")
3724                        };
3725                        format!("    {} {{ payload: {payload_type} }},", variant.name)
3726                    } else {
3727                        format!("    {},", variant.name)
3728                    }
3729                })
3730                .chain(std::iter::once(
3731                    "    Unknown(UnknownDomainError),".to_owned(),
3732                ))
3733                .collect::<Vec<_>>();
3734            format!(
3735                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
3736                variants.join("\n")
3737            )
3738        };
3739        types.declarations.push(error_definition);
3740        error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
3741        wire_codecs.push(generate_rust_wire_codecs(
3742            &operation.name,
3743            &request_type,
3744            &response_type,
3745            &error_name,
3746        ));
3747        if operation.interaction == "event" {
3748            wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
3749        }
3750        let operation_const = screaming_snake_case(&operation.name);
3751        operation_markers.push(match operation.interaction.as_str() {
3752            "request" => {
3753                let provider_method = rust_field_name(&operation.name);
3754                format!(
3755            "#[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"
3756                )
3757            }
3758            "stream" => format!(
3759                "#[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"
3760            ),
3761            "event" => format!(
3762                "#[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"
3763            ),
3764            _ => unreachable!("Descriptor validation restricts interactions"),
3765        });
3766        if operation.interaction == "request" {
3767            let invocation_error_name = if contract.operations.len() == 1 {
3768                format!("{capability_name}InvocationError")
3769            } else {
3770                format!("{capability_name}{operation_name}InvocationError")
3771            };
3772            operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
3773            provider_methods.push(format!(
3774                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
3775                rust_field_name(&operation.name),
3776            ));
3777            let field = rust_field_name(&operation.name);
3778            let conversion = format!("__LensoInto{capability_name}{operation_name}Result");
3779            provider_result_conversions.push(format!(
3780                "#[doc(hidden)]\npub trait {conversion} {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure>;\n}}\nimpl {conversion} for Result<{response_type}, {error_name}> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{ Ok(self) }}\n}}\nimpl {conversion} for Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{ self }}\n}}\nimpl {conversion} for Result<{response_type}, lenso_plugin_authoring::PluginError<{error_name}, RuntimeFailure>> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{\n        match self {{\n            Ok(value) => Ok(Ok(value)),\n            Err(lenso_plugin_authoring::PluginError::Domain(error)) => Ok(Err(error)),\n            Err(lenso_plugin_authoring::PluginError::Runtime(error)) => Err(error),\n        }}\n    }}\n}}\nimpl {conversion} for Result<{response_type}, {invocation_error_name}> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{\n        match self {{\n            Ok(value) => Ok(Ok(value)),\n            Err({invocation_error_name}::Domain(error)) => Ok(Err(error)),\n            Err({invocation_error_name}::Runtime(error)) => Err(error),\n        }}\n    }}\n}}\n"
3781            ));
3782            provider_lowering_methods.push(format!(
3783                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let plugin = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$plugin>::{field}(&plugin, context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3784            ));
3785            object_provider_lowering_methods.push(format!(
3786                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                let result = <$plugin>::{field}(plugin.as_ref(), context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3787            ));
3788            trait_object_provider_lowering_methods.push(format!(
3789                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                <$plugin as $crate::{capability_name}Provider>::{field}(plugin.as_ref(), context, request).await\n            }})\n        }}"
3790            ));
3791            endpoint_arms.push(format!(
3792                "            {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            }}",
3793                rust_field_name(&operation.name),
3794            ));
3795            let field = rust_field_name(&operation.name);
3796            client_fields.push(format!("    {field}: NativeRequestHandle<{marker_name}>,"));
3797            client_initializers.push(format!(
3798                "            {field}: dependencies.one::<{marker_name}>()?,"
3799            ));
3800            many_client_initializers.push(format!(
3801                "                    {field}: binding.handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3802            ));
3803            client_methods.push(format!(
3804                "    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    }}",
3805                screaming_snake_case(&operation.name),
3806                screaming_snake_case(&operation.name),
3807            ));
3808            invocation_errors.push(format!(
3809                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
3810            ));
3811        } else if operation.interaction == "stream" {
3812            let invocation_error_name = if contract.operations.len() == 1 {
3813                format!("{capability_name}InvocationError")
3814            } else {
3815                format!("{capability_name}{operation_name}InvocationError")
3816            };
3817            stream_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
3818            provider_methods.push(format!(
3819                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
3820                rust_field_name(&operation.name)
3821            ));
3822            let field = rust_field_name(&operation.name);
3823            let conversion = format!("__LensoInto{capability_name}{operation_name}StreamResult");
3824            provider_result_conversions.push(format!(
3825                "#[doc(hidden)]\npub trait {conversion} {{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}>;\n}}\nimpl<S> {conversion} for Result<S, {error_name}>\nwhere\n    S: NativeStreamSession + 'static,\n{{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}> {{\n        self.map(|stream| Box::new(stream) as Box<dyn NativeStreamSession>)\n            .map_err({invocation_error_name}::Domain)\n    }}\n}}\nimpl<S> {conversion} for Result<S, lenso_plugin_authoring::PluginError<{error_name}, RuntimeFailure>>\nwhere\n    S: NativeStreamSession + 'static,\n{{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}> {{\n        match self {{\n            Ok(stream) => Ok(Box::new(stream) as Box<dyn NativeStreamSession>),\n            Err(lenso_plugin_authoring::PluginError::Domain(error)) => Err({invocation_error_name}::Domain(error)),\n            Err(lenso_plugin_authoring::PluginError::Runtime(error)) => Err({invocation_error_name}::Runtime(error)),\n        }}\n    }}\n}}\nimpl<S> {conversion} for Result<S, {invocation_error_name}>\nwhere\n    S: NativeStreamSession + 'static,\n{{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}> {{\n        self.map(|stream| Box::new(stream) as Box<dyn NativeStreamSession>)\n    }}\n}}\n"
3826            ));
3827            provider_lowering_methods.push(format!(
3828                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n            let plugin = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$plugin>::{field}(&plugin, context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3829            ));
3830            object_provider_lowering_methods.push(format!(
3831                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get().map_err($crate::{invocation_error_name}::Runtime)?;\n                let result = <$plugin>::{field}(plugin.as_ref(), context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3832            ));
3833            trait_object_provider_lowering_methods.push(format!(
3834                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get().map_err($crate::{invocation_error_name}::Runtime)?;\n                <$plugin as $crate::{capability_name}Provider>::{field}(plugin.as_ref(), context, request).await\n            }})\n        }}"
3835            ));
3836            stream_endpoint_arms.push(format!(
3837                "            {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            }}",
3838                rust_field_name(&operation.name),
3839            ));
3840            let field = rust_field_name(&operation.name);
3841            client_fields.push(format!("    {field}: NativeStreamHandle<{marker_name}>,"));
3842            client_initializers.push(format!(
3843                "            {field}: dependencies.one_stream::<{marker_name}>()?,"
3844            ));
3845            many_client_initializers.push(format!(
3846                "                    {field}: binding.stream_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3847            ));
3848            client_methods.push(format!(
3849                "    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    }}"
3850            ));
3851            invocation_errors.push(format!(
3852                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
3853            ));
3854        } else {
3855            event_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
3856            provider_methods.push(format!(
3857                "    fn {}(&self, context: InvocationContext, event: {request_type}) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;",
3858                rust_field_name(&operation.name)
3859            ));
3860            let field = rust_field_name(&operation.name);
3861            let conversion = format!("__LensoInto{capability_name}{operation_name}EventResult");
3862            provider_result_conversions.push(format!(
3863                "#[doc(hidden)]\npub trait {conversion} {{\n    fn __lenso_into_result(self) -> Result<(), RuntimeFailure>;\n}}\nimpl {conversion} for () {{\n    fn __lenso_into_result(self) -> Result<(), RuntimeFailure> {{ Ok(()) }}\n}}\nimpl {conversion} for Result<(), RuntimeFailure> {{\n    fn __lenso_into_result(self) -> Result<(), RuntimeFailure> {{ self }}\n}}\n"
3864            ));
3865            provider_lowering_methods.push(format!(
3866                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let plugin = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$plugin>::{field}(&plugin, context, event).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3867            ));
3868            object_provider_lowering_methods.push(format!(
3869                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                let result = <$plugin>::{field}(plugin.as_ref(), context, event).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3870            ));
3871            trait_object_provider_lowering_methods.push(format!(
3872                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                <$plugin as $crate::{capability_name}Provider>::{field}(plugin.as_ref(), context, event).await\n            }})\n        }}"
3873            ));
3874            event_endpoint_arms.push(format!(
3875                "            {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                Rc::clone(&self.provider).{}(context, *event)\n            }}",
3876                rust_field_name(&operation.name),
3877            ));
3878            let field = rust_field_name(&operation.name);
3879            client_fields.push(format!("    {field}: NativeEventHandle<{marker_name}>,"));
3880            client_initializers.push(format!(
3881                "            {field}: dependencies.many_event::<{marker_name}>()?,"
3882            ));
3883            many_client_initializers.push(format!(
3884                "                    {field}: binding.event_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3885            ));
3886            client_methods.push(format!(
3887                "    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    }}"
3888            ));
3889        }
3890    }
3891
3892    let request_endpoint_impl = if operation_rows.is_empty() {
3893        String::new()
3894    } else {
3895        format!(
3896            "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",
3897            operation_rows.concat(),
3898            endpoint_arms.join(",\n")
3899        )
3900    };
3901    let stream_endpoint_impl = if stream_operation_rows.is_empty() {
3902        String::new()
3903    } else {
3904        format!(
3905            "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",
3906            stream_operation_rows.concat(),
3907            stream_endpoint_arms.join(",\n")
3908        )
3909    };
3910    let event_endpoint_impl = if event_operation_rows.is_empty() {
3911        String::new()
3912    } else {
3913        format!(
3914            "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",
3915            event_operation_rows.concat(),
3916            event_endpoint_arms.join(",\n")
3917        )
3918    };
3919    let mut output = String::new();
3920    output.push_str(GENERATED_HEADER);
3921    let has_request_operations = !operation_rows.is_empty();
3922    let has_stream_operations = !stream_operation_rows.is_empty();
3923    let has_event_operations = !event_operation_rows.is_empty();
3924    let mut kernel_imports = vec!["InvocationContext", "PluginDependencies", "RuntimeFailure"];
3925    if has_request_operations {
3926        kernel_imports.extend([
3927            "NativeRequestFuture",
3928            "NativeRequestEndpoint",
3929            "NativeRequestHandle",
3930            "RequestCapability",
3931        ]);
3932    }
3933    if has_stream_operations {
3934        kernel_imports.extend([
3935            "NativeStream",
3936            "NativeStreamEndpoint",
3937            "NativeStreamHandle",
3938            "NativeStreamSession",
3939            "StreamCapability",
3940            "StreamEvent",
3941        ]);
3942    }
3943    if has_event_operations {
3944        kernel_imports.extend([
3945            "EventCapability",
3946            "EventPublishResult",
3947            "NativeEventEndpoint",
3948            "NativeEventHandle",
3949        ]);
3950    }
3951    kernel_imports.sort_unstable();
3952    writeln!(
3953        output,
3954        "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
3955        kernel_imports.join(", ")
3956    )
3957    .expect("writing to a String cannot fail");
3958    output.push_str(
3959        "use lenso_plugin_authoring::{BoundCapabilityClient, CapabilityClient, CapabilityClientMany, CapabilityReference};\n",
3960    );
3961    writeln!(
3962        output,
3963        "pub const CAPABILITY_ID: &str = {};",
3964        quote_string(&contract.capability_id)
3965    )
3966    .expect("writing to a String cannot fail");
3967    writeln!(
3968        output,
3969        "pub const DESCRIPTOR_VERSION: &str = {};",
3970        quote_string(&contract.version)
3971    )
3972    .expect("writing to a String cannot fail");
3973    writeln!(
3974        output,
3975        "pub const DESCRIPTOR_DIGEST: &str = {};",
3976        quote_string(&contract.descriptor_digest)
3977    )
3978    .expect("writing to a String cannot fail");
3979    writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
3980        .expect("writing to a String cannot fail");
3981    writeln!(
3982        output,
3983        "pub const CROSS_LANE_TRANSFER: bool = {};",
3984        contract.cross_lane_transfer
3985    )
3986    .expect("writing to a String cannot fail");
3987    writeln!(
3988        output,
3989        "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
3990    )
3991    .expect("writing to a String cannot fail");
3992    write!(
3993        output,
3994        "pub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\npub const {capability_const}_DESCRIPTOR_DIGEST: &str = DESCRIPTOR_DIGEST;\npub const {capability_const}_CONTRACT: CapabilityReference<{capability_name}Client> = CapabilityReference::new(CAPABILITY_ID, DESCRIPTOR_VERSION, DESCRIPTOR_DIGEST);\n\n"
3995    )
3996    .expect("writing to a String cannot fail");
3997
3998    let capability_macro_name = snake_case(&capability_name);
3999    let client_macro_name = snake_case(&format!("{capability_name}Client"));
4000    let operations = contract
4001        .operations
4002        .iter()
4003        .map(|operation| Value::String(operation.name.clone()))
4004        .collect::<Vec<_>>();
4005    let operation_kinds = contract
4006        .operations
4007        .iter()
4008        .filter(|operation| operation.interaction != "request")
4009        .map(|operation| {
4010            (
4011                operation.name.clone(),
4012                Value::String(operation.interaction.clone()),
4013            )
4014        })
4015        .collect::<Map<_, _>>();
4016    let provided_fragment = canonical_json(&serde_json::json!({
4017        "capability_id": contract.capability_id,
4018        "descriptor_version": contract.version,
4019        "operations": operations,
4020        "operation_kinds": operation_kinds,
4021        "default_admission": {
4022            "queue_capacity": 0,
4023            "max_concurrency": 1
4024        },
4025        "operation_admissions": {},
4026        "event_admission": null,
4027        "cross_lane_transfer": contract.cross_lane_transfer
4028    }));
4029    let required_fragment = canonical_json(&serde_json::json!({
4030        "capability_id": contract.capability_id,
4031        "descriptor_version": contract.version,
4032        "cardinality": "one"
4033    }));
4034    let required_many_fragment = canonical_json(&serde_json::json!({
4035        "capability_id": contract.capability_id,
4036        "descriptor_version": contract.version,
4037        "cardinality": "many"
4038    }));
4039    let named_requirement_suffix = |cardinality: &str| {
4040        format!(
4041            ",\"capability_id\":{},\"descriptor_version\":{},\"cardinality\":{}}}",
4042            quote_string(&contract.capability_id),
4043            quote_string(&contract.version),
4044            quote_string(cardinality),
4045        )
4046    };
4047    let required_named_suffix = named_requirement_suffix("one");
4048    let required_optional_named_suffix = named_requirement_suffix("optional");
4049    let required_many_named_suffix = named_requirement_suffix("many");
4050    writeln!(
4051        output,
4052        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_provided_{capability_macro_name} {{ () => {{ {} }}; }}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_required_{client_macro_name} {{\n    () => {{ {} }};\n    ($requirement_id:literal) => {{ concat!(\"{{\\\"requirement_id\\\":\", stringify!($requirement_id), {}) }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_required_optional_{client_macro_name} {{\n    ($requirement_id:literal) => {{ concat!(\"{{\\\"requirement_id\\\":\", stringify!($requirement_id), {}) }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_required_many_{client_macro_name} {{\n    () => {{ {} }};\n    ($requirement_id:literal) => {{ concat!(\"{{\\\"requirement_id\\\":\", stringify!($requirement_id), {}) }};\n}}\n",
4053        quote_string(&provided_fragment),
4054        quote_string(&required_fragment),
4055        quote_string(&required_named_suffix),
4056        quote_string(&required_optional_named_suffix),
4057        quote_string(&required_many_fragment),
4058        quote_string(&required_many_named_suffix),
4059    )
4060    .expect("writing to a String cannot fail");
4061    for operation in &contract.operations {
4062        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4063        writeln!(
4064            output,
4065            "pub const {operation_const}: &str = {};",
4066            quote_string(&operation.name)
4067        )
4068        .expect("writing to a String cannot fail");
4069    }
4070    writeln!(
4071        output,
4072        "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
4073        rust_runtime_types(contract).join(", ")
4074    )
4075    .expect("writing to a String cannot fail");
4076    for declaration in types.declarations {
4077        output.push_str(&declaration);
4078        output.push('\n');
4079    }
4080    for marker in operation_markers {
4081        output.push_str(&marker);
4082        output.push('\n');
4083    }
4084    for codec in error_codecs {
4085        output.push_str(&codec);
4086        output.push('\n');
4087    }
4088    for codec in wire_codecs {
4089        output.push_str(&codec);
4090        output.push('\n');
4091    }
4092    for conversion in provider_result_conversions {
4093        output.push_str(&conversion);
4094        output.push('\n');
4095    }
4096    write!(
4097        output,
4098        "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
4099        provider_methods.join("\n")
4100    )
4101    .expect("writing to a String cannot fail");
4102    writeln!(
4103        output,
4104        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_{capability_macro_name} {{\n    ($plugin:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $plugin {{\n{}\n        }}\n    }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_object_{capability_macro_name} {{\n    ($object:ty, $plugin:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $object {{\n{}\n        }}\n    }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_trait_object_{capability_macro_name} {{\n    ($object:ty, $plugin:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $object {{\n{}\n        }}\n    }};\n}}\n",
4105        provider_lowering_methods.join("\n"),
4106        object_provider_lowering_methods.join("\n"),
4107        trait_object_provider_lowering_methods.join("\n")
4108    )
4109    .expect("writing to a String cannot fail");
4110    if has_request_operations {
4111        write!(
4112            output,
4113            "#[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"
4114        )
4115        .expect("writing to a String cannot fail");
4116    } else {
4117        write!(
4118            output,
4119            "#[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"
4120        )
4121        .expect("writing to a String cannot fail");
4122    }
4123    output.push_str(&request_endpoint_impl);
4124    output.push_str(&stream_endpoint_impl);
4125    output.push_str(&event_endpoint_impl);
4126    let request_endpoint_value = has_request_operations.then_some(
4127        "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeRequestEndpoint>",
4128    );
4129    let stream_endpoint_value = has_stream_operations.then_some(
4130        "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeStreamEndpoint>",
4131    );
4132    let event_endpoint_value = has_event_operations
4133        .then_some("endpoint as ::std::rc::Rc<dyn __LensoNativeSupport::NativeEventEndpoint>");
4134    writeln!(
4135        output,
4136        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_endpoints_{capability_macro_name} {{\n    ($provider:expr, $support:path) => {{{{\n        use $support as __LensoNativeSupport;\n        let endpoint = ::std::rc::Rc::new($crate::{capability_name}Endpoint::new($provider));\n        (\n            vec![{}],\n            vec![{}],\n            vec![{}],\n        )\n    }}}};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_provide_{capability_macro_name} {{\n    ($provider:expr, $lifecycle:expr, $support:path) => {{{{\n        use $support as __LensoNativeSupport;\n        let (request_endpoints, stream_endpoints, event_endpoints) =\n            $crate::__lenso_native_endpoints_{capability_macro_name}!($provider, $support);\n        __LensoNativeSupport::NativePluginInstance::with_all_endpoints(\n            request_endpoints,\n            stream_endpoints,\n            event_endpoints,\n            $lifecycle,\n        )\n    }}}};\n}}\n",
4137        request_endpoint_value.unwrap_or_default(),
4138        stream_endpoint_value.unwrap_or_default(),
4139        event_endpoint_value.unwrap_or_default(),
4140    )
4141    .expect("writing to a String cannot fail");
4142    let new_method = if contract.operations.len() == 1 {
4143        let field = rust_field_name(&contract.operations[0].name);
4144        let marker = &capability_name;
4145        match contract.operations[0].interaction.as_str() {
4146            "request" => format!(
4147                "    pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
4148            ),
4149            "stream" => format!(
4150                "    pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
4151            ),
4152            "event" => format!(
4153                "    pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
4154            ),
4155            _ => unreachable!("Descriptor validation restricts interactions"),
4156        }
4157    } else {
4158        String::new()
4159    };
4160    write!(
4161        output,
4162        "#[derive(Debug)]\npub struct {capability_name}Client {{\n{}\n}}\nimpl {capability_name}Client {{\n{}    pub fn from_dependencies(dependencies: &PluginDependencies) -> Result<Self, RuntimeFailure> {{\n        <Self as CapabilityClient>::from_dependencies(dependencies)\n    }}\n\n    pub fn from_requirement(\n        dependencies: &PluginDependencies,\n        requirement_id: &str,\n    ) -> Result<Self, RuntimeFailure> {{\n        <Self as CapabilityClient>::from_requirement(dependencies, requirement_id)\n    }}\n\n{}\n}}\n\nimpl CapabilityClient for {capability_name}Client {{\n    type Dependencies = PluginDependencies;\n    type Error = RuntimeFailure;\n\n    const CAPABILITY_ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n\n    fn from_dependencies(dependencies: &PluginDependencies) -> Result<Self, RuntimeFailure> {{\n        Ok(Self {{\n{}\n        }})\n    }}\n\n    fn from_requirement(\n        dependencies: &PluginDependencies,\n        requirement_id: &str,\n    ) -> Result<Self, RuntimeFailure> {{\n        let dependencies = dependencies.requirement(requirement_id)?;\n        Self::from_dependencies(&dependencies)\n    }}\n\n    fn already_connected() -> RuntimeFailure {{\n        RuntimeFailure::PluginFailure {{\n            detail: format!(\"Capability Port {{CAPABILITY_ID}} was connected more than once\"),\n        }}\n    }}\n}}\n\nimpl CapabilityClientMany for {capability_name}Client {{\n    fn many_from_dependencies(\n        dependencies: &PluginDependencies,\n    ) -> Result<Vec<BoundCapabilityClient<Self>>, RuntimeFailure> {{\n        dependencies\n            .bindings()\n            .iter()\n            .filter(|binding| binding.capability_id() == CAPABILITY_ID)\n            .map(|binding| {{\n                Ok(BoundCapabilityClient::new(\n                    binding.provider_instance(),\n                    Self {{\n{}\n                    }},\n                ))\n            }})\n            .collect()\n    }}\n\n    fn many_from_requirement(\n        dependencies: &PluginDependencies,\n        requirement_id: &str,\n    ) -> Result<Vec<BoundCapabilityClient<Self>>, RuntimeFailure> {{\n        let dependencies = dependencies.requirement(requirement_id)?;\n        Self::many_from_dependencies(&dependencies)\n    }}\n}}\n\n",
4163        client_fields.join("\n"),
4164        new_method,
4165        client_methods.join("\n\n"),
4166        client_initializers.join("\n"),
4167        many_client_initializers.join("\n")
4168    )
4169    .expect("writing to a String cannot fail");
4170    for error in invocation_errors {
4171        output.push_str(&error);
4172    }
4173    format!("{}\n", output.trim_end())
4174}
4175
4176fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
4177    let mut types = BTreeSet::from(["UnknownDomainError"]);
4178    for operation in &contract.operations {
4179        collect_rust_runtime_types(&operation.request, &mut types);
4180        collect_rust_runtime_types(&operation.response, &mut types);
4181        for error in &operation.domain_errors {
4182            if let Some(payload) = &error.payload {
4183                if !error.payload_required && payload.is_nullable() {
4184                    types.insert("OptionalValue");
4185                }
4186                collect_rust_runtime_types(payload, &mut types);
4187            }
4188        }
4189    }
4190    types.into_iter().collect()
4191}
4192
4193fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
4194    match ty {
4195        TypeIr::Int64 => {
4196            types.insert("Int64");
4197        }
4198        TypeIr::Uint64 => {
4199            types.insert("Uint64");
4200        }
4201        TypeIr::Bytes => {
4202            types.insert("Bytes");
4203        }
4204        TypeIr::Timestamp => {
4205            types.insert("Timestamp");
4206        }
4207        TypeIr::Duration => {
4208            types.insert("Duration");
4209        }
4210        TypeIr::RawJson => {
4211            types.insert("RawJson");
4212        }
4213        TypeIr::Array(item) | TypeIr::Nullable(item) => {
4214            collect_rust_runtime_types(item, types);
4215        }
4216        TypeIr::Object {
4217            fields, additional, ..
4218        } => {
4219            for field in fields {
4220                if !field.required && field.ty.is_nullable() {
4221                    types.insert("OptionalValue");
4222                }
4223                collect_rust_runtime_types(&field.ty, types);
4224            }
4225            if let ObjectAdditionalIr::Typed(value) = additional {
4226                collect_rust_runtime_types(value, types);
4227            }
4228        }
4229        TypeIr::Any
4230        | TypeIr::String
4231        | TypeIr::Integer
4232        | TypeIr::Number
4233        | TypeIr::Boolean
4234        | TypeIr::Null
4235        | TypeIr::Enum { .. } => {}
4236    }
4237}
4238
4239#[allow(clippy::too_many_lines)]
4240fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
4241    let mut output = String::new();
4242    writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
4243    output.push_str(
4244        "    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n",
4245    );
4246    output.push_str("        use serde::ser::SerializeMap;\n");
4247    output.push_str("        match self {\n");
4248    for variant in variants {
4249        if variant.structured {
4250            if variant.payload.is_some() {
4251                if variant.payload_required {
4252                    writeln!(
4253                        output,
4254                        "            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            }},",
4255                        variant.name,
4256                        quote_string(&variant.code)
4257                    )
4258                    .expect("String cannot fail");
4259                } else {
4260                    writeln!(
4261                        output,
4262                        "            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            }},",
4263                        variant.name,
4264                        quote_string(&variant.code)
4265                    )
4266                    .expect("String cannot fail");
4267                }
4268            } else {
4269                writeln!(
4270                    output,
4271                    "            Self::{} => {{\n                let mut map = serializer.serialize_map(Some(1))?;\n                map.serialize_entry(\"code\", {})?;\n                map.end()\n            }},",
4272                    variant.name,
4273                    quote_string(&variant.code)
4274                )
4275                .expect("String cannot fail");
4276            }
4277        } else {
4278            writeln!(
4279                output,
4280                "            Self::{} => serializer.serialize_str({}),",
4281                variant.name,
4282                quote_string(&variant.code)
4283            )
4284            .expect("String cannot fail");
4285        }
4286    }
4287    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");
4288    writeln!(
4289        output,
4290        "impl<'de> serde::Deserialize<'de> for {error_name} {{"
4291    )
4292    .expect("String cannot fail");
4293    output.push_str(
4294        "    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",
4295    );
4296    for variant in variants.iter().filter(|variant| !variant.structured) {
4297        writeln!(
4298            output,
4299            "                {} => Ok(Self::{}),",
4300            quote_string(&variant.code),
4301            variant.name
4302        )
4303        .expect("String cannot fail");
4304    }
4305    output.push_str(
4306        "                _ => 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",
4307    );
4308    if variants.iter().any(|variant| variant.structured) {
4309        output.push_str("                match code.as_str() {\n");
4310        for variant in variants.iter().filter(|variant| variant.structured) {
4311            if variant.payload.is_some() {
4312                if variant.payload_required {
4313                    writeln!(
4314                        output,
4315                        "                    {} => {{\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                    }},",
4316                        quote_string(&variant.code),
4317                        variant.name
4318                    )
4319                    .expect("String cannot fail");
4320                } else {
4321                    writeln!(
4322                        output,
4323                        "                    {} => {{\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                    }},",
4324                        quote_string(&variant.code),
4325                        variant.name
4326                    )
4327                    .expect("String cannot fail");
4328                }
4329            } else {
4330                writeln!(
4331                    output,
4332                    "                    {} => Ok(Self::{}),",
4333                    quote_string(&variant.code),
4334                    variant.name
4335                )
4336                .expect("String cannot fail");
4337            }
4338        }
4339        output.push_str(
4340            "                    _ => {\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",
4341        );
4342    } else {
4343        output.push_str(
4344            "                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",
4345        );
4346    }
4347    output.push_str(
4348        "            other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n        }\n    }\n}\n",
4349    );
4350    output
4351}
4352
4353#[allow(clippy::too_many_lines)]
4354fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
4355    let capability_name = pascal_case(
4356        contract
4357            .capability_id
4358            .split('@')
4359            .next()
4360            .and_then(|identity| identity.rsplit('.').next())
4361            .unwrap_or("Capability"),
4362    );
4363    let codec_name = format!("{capability_name}JsonCodec");
4364    let guest_client_name = format!("{capability_name}GuestClient");
4365    let mut types = RustTypes::new();
4366    let mut request_operations = Vec::new();
4367    let mut stream_operations = Vec::new();
4368    let mut encode_arms = Vec::new();
4369    let mut response_arms = Vec::new();
4370    let mut error_arms = Vec::new();
4371    let mut host_request_arms = Vec::new();
4372    let mut stream_open_arms = Vec::new();
4373    let mut stream_encode_arms = Vec::new();
4374    let mut stream_decode_arms = Vec::new();
4375    let mut stream_error_arms = Vec::new();
4376    let mut host_stream_arms = Vec::new();
4377    let mut guest_methods = Vec::new();
4378
4379    for operation in &contract.operations {
4380        if operation.interaction == "event" {
4381            return Err(CodegenError::UnsupportedInteraction {
4382                operation: operation.name.clone(),
4383                interaction: operation.interaction.clone(),
4384            });
4385        }
4386        let operation_name = pascal_case(&operation.name);
4387        let marker_name = if contract.operations.len() == 1 {
4388            capability_name.clone()
4389        } else {
4390            format!("{capability_name}{operation_name}")
4391        };
4392        let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
4393        let response_type =
4394            types.type_for(&operation.response, &format!("{operation_name}Response"));
4395        let error_type = format!("{operation_name}Error");
4396        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4397        let method_name = rust_field_name(&operation.name);
4398        match operation.interaction.as_str() {
4399            "request" => {
4400                request_operations.push(operation_const.clone());
4401                encode_arms.push(format!(
4402                    "            {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            }}"
4403                ));
4404                response_arms.push(format!(
4405                    "            {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()),"
4406                ));
4407                error_arms.push(format!(
4408                    "            {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()),"
4409                ));
4410                host_request_arms.push(format!(
4411                    "            {operation_const} => {{\n                let request = serde_json::from_value::<{request_type}>(request).map_err(|_| runtime_codec_protocol_failure());\n                Box::pin(async move {{\n                    let request = request?;\n                    let handle = dependency.typed::<{marker_name}>()?;\n                    match handle.invoke_with_context({operation_const}, context, request).await? {{\n                        Ok(response) => serde_json::to_value(response)\n                            .map(lenso_runtime_codec::JsonInvocationOutcome::Success)\n                            .map_err(|_| runtime_codec_protocol_failure()),\n                        Err(error) => serde_json::to_value(error)\n                            .map(lenso_runtime_codec::JsonInvocationOutcome::DomainError)\n                            .map_err(|_| runtime_codec_protocol_failure()),\n                    }}\n                }})\n            }}"
4412                ));
4413                guest_methods.push(format!(
4414                    "    pub fn {method_name}(&self, request: &{request_type}) -> Result<{response_type}, lenso_guest_sdk::GuestError<{error_type}>> {{\n        self.capability.request({operation_const}, request)\n    }}"
4415                ));
4416            }
4417            "stream" => {
4418                stream_operations.push(operation_const.clone());
4419                stream_open_arms.push(format!(
4420                    "            {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            }}"
4421                ));
4422                stream_encode_arms.push(format!(
4423                    "            {operation_const} => {{\n                let value = message.downcast_ref::<{response_type}>().ok_or_else(runtime_codec_protocol_failure)?;\n                serde_json::to_value(value).map_err(|_| runtime_codec_protocol_failure())\n            }}"
4424                ));
4425                stream_decode_arms.push(format!(
4426                    "            {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()),"
4427                ));
4428                stream_error_arms.push(format!(
4429                    "            {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()),"
4430                ));
4431                host_stream_arms.push(format!(
4432                    "            {operation_const} => {{\n                let request = serde_json::from_value::<{request_type}>(request).map_err(|_| runtime_codec_protocol_failure());\n                Box::pin(async move {{\n                    let request = request?;\n                    let handle = dependency.typed::<{marker_name}>()?;\n                    match handle.open_with_context({operation_const}, context, request).await? {{\n                        Ok(stream) => Ok(Ok(lenso_runtime_codec::json_host_stream::<{marker_name}>(\n                            stream,\n                            |value| serde_json::from_value::<{response_type}>(value).map_err(|_| runtime_codec_protocol_failure()),\n                            |message| serde_json::to_value(message).map_err(|_| runtime_codec_protocol_failure()),\n                            |error| serde_json::to_value(error).map_err(|_| runtime_codec_protocol_failure()),\n                        ))),\n                        Err(error) => serde_json::to_value(error)\n                            .map(Err)\n                            .map_err(|_| runtime_codec_protocol_failure()),\n                    }}\n                }})\n            }}"
4433                ));
4434                guest_methods.push(format!(
4435                    "    pub fn {method_name}(&self, request: &{request_type}) -> Result<lenso_guest_sdk::GuestStream<H, {response_type}, {error_type}>, lenso_guest_sdk::GuestError<{error_type}>> {{\n        self.capability.open_stream({operation_const}, request)\n    }}"
4436                ));
4437            }
4438            _ => unreachable!("Descriptor validation restricts interactions"),
4439        }
4440    }
4441
4442    let encode_dispatch = runtime_codec_dispatch(&encode_arms);
4443    let response_dispatch = runtime_codec_dispatch(&response_arms);
4444    let error_dispatch = runtime_codec_dispatch(&error_arms);
4445    let host_request_dispatch = runtime_codec_host_dispatch(&host_request_arms);
4446    let stream_open_dispatch = runtime_codec_dispatch(&stream_open_arms);
4447    let stream_encode_dispatch = runtime_codec_dispatch(&stream_encode_arms);
4448    let stream_decode_dispatch = runtime_codec_dispatch(&stream_decode_arms);
4449    let stream_error_dispatch = runtime_codec_dispatch(&stream_error_arms);
4450    let host_stream_dispatch = runtime_codec_host_dispatch(&host_stream_arms);
4451    let request_parameter = if request_operations.is_empty() {
4452        "_request"
4453    } else {
4454        "request"
4455    };
4456    let request_value_parameter = if request_operations.is_empty() {
4457        "_value"
4458    } else {
4459        "value"
4460    };
4461    let stream_request_parameter = if stream_operations.is_empty() {
4462        "_request"
4463    } else {
4464        "request"
4465    };
4466    let stream_message_parameter = if stream_operations.is_empty() {
4467        "_message"
4468    } else {
4469        "message"
4470    };
4471    let stream_value_parameter = if stream_operations.is_empty() {
4472        "_value"
4473    } else {
4474        "value"
4475    };
4476    let (host_request_dependency_parameter, host_request_parameter, host_request_context_parameter) =
4477        if request_operations.is_empty() {
4478            ("_dependency", "_request", "_context")
4479        } else {
4480            ("dependency", "request", "context")
4481        };
4482    let (host_stream_dependency_parameter, host_stream_parameter, host_stream_context_parameter) =
4483        if stream_operations.is_empty() {
4484            ("_dependency", "_request", "_context")
4485        } else {
4486            ("dependency", "request", "context")
4487        };
4488
4489    let mut output = generate_rust(contract);
4490    write!(
4491        output,
4492        "\n#[derive(Clone, Copy, Debug)]\npub struct {guest_client_name}<'a, H: lenso_guest_sdk::HostImports> {{\n    capability: lenso_guest_sdk::GuestCapability<'a, H>,\n}}\n\nimpl<'a, H: lenso_guest_sdk::HostImports> {guest_client_name}<'a, H> {{\n    pub fn from_context(context: &'a lenso_guest_sdk::GuestContext<H>) -> Result<Self, lenso_guest_sdk::GuestError<serde_json::Value>> {{\n        context\n            .require(CAPABILITY_ID, DESCRIPTOR_VERSION, &[{}], &[{}])\n            .map(|capability| Self {{ capability }})\n    }}\n\n{}\n}}\n",
4493        request_operations.join(", "),
4494        stream_operations.join(", "),
4495        guest_methods.join("\n\n"),
4496    )
4497    .expect("writing generated Rust to a String cannot fail");
4498    write!(
4499        output,
4500        "\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 descriptor_digest(&self) -> &'static str {{ DESCRIPTOR_DIGEST }}\n\n    fn request_operations(&self) -> &'static [&'static str] {{ &[{}] }}\n    fn stream_operations(&self) -> &'static [&'static str] {{ &[{}] }}\n\n    fn encode_request(&self, operation: &str, {request_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{encode_dispatch}\n    }}\n\n    fn decode_response(&self, operation: &str, {request_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{response_dispatch}\n    }}\n\n    fn decode_domain_error(&self, operation: &str, {request_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{error_dispatch}\n    }}\n\n    fn encode_stream_open(&self, operation: &str, {stream_request_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{stream_open_dispatch}\n    }}\n\n    fn encode_stream_message(&self, operation: &str, {stream_message_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{stream_encode_dispatch}\n    }}\n\n    fn decode_stream_message(&self, operation: &str, {stream_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{stream_decode_dispatch}\n    }}\n\n    fn decode_stream_domain_error(&self, operation: &str, {stream_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{stream_error_dispatch}\n    }}\n\n    fn invoke_host_request(&self, {host_request_dependency_parameter}: lenso_kernel::PluginDependencyHandle, operation: String, {host_request_parameter}: serde_json::Value, {host_request_context_parameter}: InvocationContext) -> lenso_runtime_codec::JsonHostRequestFuture {{\n{host_request_dispatch}\n    }}\n\n    fn open_host_stream(&self, {host_stream_dependency_parameter}: lenso_kernel::PluginStreamDependencyHandle, operation: String, {host_stream_parameter}: serde_json::Value, {host_stream_context_parameter}: InvocationContext) -> lenso_runtime_codec::JsonHostStreamOpenFuture {{\n{host_stream_dispatch}\n    }}\n}}\n\nfn runtime_codec_protocol_failure() -> RuntimeFailure {{ RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }} }}\n\nfn runtime_codec_unknown_operation(operation: &str) -> RuntimeFailure {{\n    RuntimeFailure::UnknownOperation {{ capability: CAPABILITY_ID, operation: operation.to_owned() }}\n}}\n",
4501        request_operations.join(", "),
4502        stream_operations.join(", "),
4503    )
4504    .expect("writing generated Rust to a String cannot fail");
4505    Ok(output)
4506}
4507
4508fn runtime_codec_host_dispatch(arms: &[String]) -> String {
4509    if arms.is_empty() {
4510        "        Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation))))"
4511            .to_owned()
4512    } else {
4513        let arms = arms
4514            .iter()
4515            .map(|arm| arm.trim_end_matches(','))
4516            .collect::<Vec<_>>()
4517            .join(",\n");
4518        format!(
4519            "        match operation.as_str() {{\n{arms},\n            _ => Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation)))),\n        }}"
4520        )
4521    }
4522}
4523
4524fn runtime_codec_dispatch(arms: &[String]) -> String {
4525    if arms.is_empty() {
4526        "        Err(runtime_codec_unknown_operation(operation))".to_owned()
4527    } else {
4528        let arms = arms
4529            .iter()
4530            .map(|arm| arm.trim_end_matches(','))
4531            .collect::<Vec<_>>()
4532            .join(",\n");
4533        format!(
4534            "        match operation {{\n{arms},\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}"
4535        )
4536    }
4537}
4538
4539fn generate_rust_wire_codecs(
4540    operation: &str,
4541    request_type: &str,
4542    response_type: &str,
4543    error_type: &str,
4544) -> String {
4545    let stem = snake_case(operation);
4546    format!(
4547        "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"
4548    )
4549}
4550
4551fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
4552    let stem = snake_case(operation);
4553    format!(
4554        "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"
4555    )
4556}
4557
4558fn generate_typescript_codecs(
4559    operation: &str,
4560    request_type: &str,
4561    response_type: &str,
4562    error_type: &str,
4563    variants: &[ErrorVariantIr],
4564) -> String {
4565    let stem = pascal_case(operation);
4566    let known_strings = variants
4567        .iter()
4568        .filter(|variant| !variant.structured)
4569        .map(|variant| quote_string(&variant.code))
4570        .collect::<Vec<_>>()
4571        .join(", ");
4572    format!(
4573        "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"
4574    )
4575}
4576
4577fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
4578    let stem = pascal_case(operation);
4579    format!(
4580        "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"
4581    )
4582}
4583
4584#[allow(clippy::too_many_lines)]
4585fn generate_typescript(contract: &ContractIr) -> String {
4586    let capability_name = pascal_case(
4587        contract
4588            .capability_id
4589            .split('@')
4590            .next()
4591            .and_then(|identity| identity.rsplit('.').next())
4592            .unwrap_or("Capability"),
4593    );
4594    let capability_const = screaming_snake_case(&capability_name);
4595    let mut types = TypeScriptTypes::new();
4596    let mut clients = Vec::new();
4597    let mut providers = Vec::new();
4598    let mut errors = Vec::new();
4599    let mut codecs = Vec::new();
4600    let mut request_dispatch_arms = Vec::new();
4601    let mut dependency_methods = Vec::new();
4602    let operation_names = contract
4603        .operations
4604        .iter()
4605        .map(|operation| quote_string(&operation.name))
4606        .collect::<Vec<_>>()
4607        .join(", ");
4608    let stream_operation_names = contract
4609        .operations
4610        .iter()
4611        .filter(|operation| operation.interaction == "stream")
4612        .map(|operation| quote_string(&operation.name))
4613        .collect::<Vec<_>>()
4614        .join(", ");
4615    let event_operation_names = contract
4616        .operations
4617        .iter()
4618        .filter(|operation| operation.interaction == "event")
4619        .map(|operation| quote_string(&operation.name))
4620        .collect::<Vec<_>>()
4621        .join(", ");
4622    let has_event_operations = contract
4623        .operations
4624        .iter()
4625        .any(|operation| operation.interaction == "event");
4626    for operation in &contract.operations {
4627        let operation_name = pascal_case(&operation.name);
4628        let request_name = format!("{operation_name}Request");
4629        let response_name = format!("{operation_name}Response");
4630        let error_name = format!("{operation_name}Error");
4631        let request_type = types.type_for(&operation.request, &request_name);
4632        let response_type = types.type_for(&operation.response, &response_name);
4633        let variants = &operation.domain_errors;
4634        let error_type = if variants.is_empty() {
4635            "UnknownDomainError".to_owned()
4636        } else {
4637            let mut values = variants
4638                .iter()
4639                .map(|variant| {
4640                    if let Some(payload) = &variant.payload {
4641                        let payload_name = format!("{error_name}{}Payload", variant.name);
4642                        let payload_type =
4643                            types.type_for_non_null(payload.non_null(), &payload_name);
4644                        let payload_type = if payload.is_nullable() {
4645                            format!("{payload_type} | null")
4646                        } else {
4647                            payload_type
4648                        };
4649                        let payload_optional = if variant.payload_required { "" } else { "?" };
4650                        format!(
4651                            "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
4652                            quote_string(&variant.code),
4653                        )
4654                    } else if variant.structured {
4655                        format!("{{ readonly code: {} }}", quote_string(&variant.code))
4656                    } else {
4657                        quote_string(&variant.code)
4658                    }
4659                })
4660                .collect::<Vec<_>>();
4661            values.push("UnknownDomainError".to_owned());
4662            values.join(" | ")
4663        };
4664        let invocation_error_name = format!("{operation_name}InvocationError");
4665        let result_name = format!("{operation_name}Result");
4666        let result_value_type = match operation.interaction.as_str() {
4667            "stream" => format!("StreamSession<{response_type}, {error_name}>"),
4668            "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
4669            _ => response_type.clone(),
4670        };
4671        if operation.interaction == "event" {
4672            errors.push(format!(
4673                "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
4674            ));
4675        } else {
4676            errors.push(format!(
4677                "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} }};"
4678            ));
4679        }
4680        codecs.push(generate_typescript_codecs(
4681            &operation.name,
4682            &request_type,
4683            &response_type,
4684            &error_name,
4685            variants,
4686        ));
4687        if operation.interaction == "event" {
4688            codecs.push(generate_typescript_event_codecs(
4689                &operation.name,
4690                &request_type,
4691            ));
4692        }
4693        if matches!(operation.interaction.as_str(), "request" | "stream") {
4694            clients.push(format!(
4695                "  {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4696                typescript_property_name(&snake_case(&operation.name)),
4697            ));
4698            providers.push(format!(
4699                "  {}(context: InvocationContext, request: {request_type}): Promise<{result_name}>;",
4700                typescript_property_name(&snake_case(&operation.name)),
4701            ));
4702        } else {
4703            clients.push(format!(
4704                "  {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4705                typescript_property_name(&snake_case(&operation.name)),
4706            ));
4707            providers.push(format!(
4708                "  {}(context: InvocationContext, event: {request_type}): void;",
4709                typescript_property_name(&snake_case(&operation.name)),
4710            ));
4711        }
4712        if operation.interaction == "request" {
4713            let provider_method = typescript_property_name(&snake_case(&operation.name));
4714            request_dispatch_arms.push(format!(
4715                "      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: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n        }}\n      }}",
4716                quote_string(&operation.name),
4717            ));
4718            dependency_methods.push(format!(
4719                "      async {provider_method}(request, context) {{\n        let payload: unknown;\n        try {{\n          payload = JSON.parse(encode{operation_name}Request(request)) as unknown;\n        }} catch (error) {{\n          return {{ ok: false, error: {{ kind: \"runtime\", error: {{ kind: \"protocol_violation\", detail: dependencyErrorMessage(error) }} }} }};\n        }}\n        const call = context ?? {{ requestId: \"0\" as Uint64, cancelled: false }};\n        try {{\n          const outcome = await invoke({}, call, payload);\n          if (outcome.kind === \"success\") {{\n            return {{ ok: true, value: decode{operation_name}Response(JSON.stringify(outcome.value)) }};\n          }}\n          if (outcome.kind === \"domain\") {{\n            return {{ ok: false, error: {{ kind: \"domain\", error: decode{operation_name}Error(JSON.stringify(outcome.value)) }} }};\n          }}\n          return {{ ok: false, error: {{ kind: \"runtime\", error: outcome.failure }} }};\n        }} catch (error) {{\n          return {{ ok: false, error: {{ kind: \"runtime\", error: {{ kind: \"plugin_failure\", detail: dependencyErrorMessage(error) }} }} }};\n        }}\n      }},",
4720                quote_string(&operation.name),
4721            ));
4722        }
4723    }
4724    let mut output = String::new();
4725    output.push_str(TYPESCRIPT_HEADER);
4726    output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
4727    writeln!(
4728        output,
4729        "export const CAPABILITY_ID = {};",
4730        quote_string(&contract.capability_id)
4731    )
4732    .expect("writing to a String cannot fail");
4733    writeln!(
4734        output,
4735        "export const DESCRIPTOR_VERSION = {};",
4736        quote_string(&contract.version)
4737    )
4738    .expect("writing to a String cannot fail");
4739    writeln!(
4740        output,
4741        "export const DESCRIPTOR_DIGEST = {};",
4742        quote_string(&contract.descriptor_digest)
4743    )
4744    .expect("writing to a String cannot fail");
4745    writeln!(output, "export const PORTABLE = {};", contract.portable)
4746        .expect("writing to a String cannot fail");
4747    write!(
4748        output,
4749        "export const CROSS_LANE_TRANSFER = {};\n\n",
4750        contract.cross_lane_transfer
4751    )
4752    .expect("writing to a String cannot fail");
4753    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\nexport interface CapabilityContractReference<Client> {\n  readonly capability_id: string;\n  readonly descriptor_version: string;\n  readonly descriptor_digest: string;\n  readonly generated_client: string;\n  readonly __client?: Client;\n}\n\n");
4754    if has_event_operations {
4755        output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
4756    }
4757    for declaration in types.declarations {
4758        output.push_str(&declaration);
4759        output.push('\n');
4760    }
4761    for error in errors {
4762        output.push_str(&error);
4763        output.push('\n');
4764    }
4765    for codec in codecs {
4766        output.push_str(&codec);
4767        output.push('\n');
4768    }
4769    write!(
4770        output,
4771        "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
4772        clients.join("\n"),
4773        providers.join("\n")
4774    )
4775    .expect("writing to a String cannot fail");
4776    writeln!(
4777        output,
4778        "\nexport const {capability_const}_CONTRACT: CapabilityContractReference<{capability_name}Client> = {{ capability_id: CAPABILITY_ID, descriptor_version: DESCRIPTOR_VERSION, descriptor_digest: DESCRIPTOR_DIGEST, generated_client: {} }};",
4779        quote_string(&format!("{capability_name}Client")),
4780    )
4781    .expect("writing to a String cannot fail");
4782    write!(
4783        output,
4784        "\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",
4785        request_dispatch_arms.join("\n")
4786    )
4787    .expect("writing to a String cannot fail");
4788    if contract
4789        .operations
4790        .iter()
4791        .all(|operation| operation.interaction == "request")
4792    {
4793        write!(
4794            output,
4795            "\nexport type DependencyInvoker = (\n  operation: string,\n  context: InvocationContext,\n  payload: unknown,\n) => Promise<ProviderDispatchOutcome>;\n\nexport interface CapabilityDependencyBinding<Client> {{\n  readonly descriptor: CapabilityProviderDescriptor;\n  createClient(invoke: DependencyInvoker): Client;\n}}\n\nfunction dependencyErrorMessage(error: unknown): string {{\n  return error instanceof Error ? error.message : String(error);\n}}\n\nexport function bind{capability_name}Dependency(): CapabilityDependencyBinding<{capability_name}Client> {{\n  return {{\n    descriptor: {{\n      capability_id: CAPABILITY_ID,\n      descriptor_version: DESCRIPTOR_VERSION,\n      operations: [{operation_names}],\n      stream_operations: [],\n      event_operations: [],\n    }},\n    createClient(invoke) {{\n      return {{\n{}\n      }};\n    }},\n  }};\n}}\n\nexport const bindDependency = bind{capability_name}Dependency;\n",
4796            dependency_methods.join("\n"),
4797        )
4798        .expect("writing to a String cannot fail");
4799    }
4800    output.push_str(
4801        "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
4802    );
4803    output
4804}