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