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