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