Skip to main content

lenso_contract_codegen/
lib.rs

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