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