Skip to main content

lenso_contract_codegen/
lib.rs

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