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},
11    error::Error,
12    fmt,
13    fmt::Write as _,
14    fs,
15    path::{Path, PathBuf},
16};
17
18use serde_json::{Map, Value};
19
20mod browser;
21mod ir;
22mod rust_debug;
23mod source;
24mod wit;
25
26pub use browser::generate_browser_request_client;
27pub use source::{check_source_snapshot, write_source_snapshot};
28
29use ir::{ContractIr, ErrorVariantIr, FieldIr, ObjectAdditionalIr, OperationIr, TypeIr};
30
31const GENERATED_HEADER: &str = "// @generated by lenso-contract-codegen; do not edit.\n";
32const TYPESCRIPT_HEADER: &str = "/* @generated by lenso-contract-codegen; do not edit. */\n";
33const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
34
35/// A parsed Descriptor `SemVer`.
36#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
37struct Version(semver::Version);
38
39impl Version {
40    fn parse(value: &str) -> Result<Self, CodegenError> {
41        semver::Version::parse(value)
42            .map(Self)
43            .map_err(|error| CodegenError::InvalidDescriptor {
44                detail: format!("Descriptor version `{value}` is not valid SemVer: {error}"),
45            })
46    }
47
48    const fn major(&self) -> u64 {
49        self.0.major
50    }
51
52    const fn minor(&self) -> u64 {
53        self.0.minor
54    }
55}
56
57fn parse_numeric_component(value: &str) -> Option<u64> {
58    (!value.is_empty() && (value == "0" || !value.starts_with('0')))
59        .then(|| value.parse().ok())
60        .flatten()
61}
62
63#[derive(Clone, Debug)]
64struct Operation {
65    name: String,
66    interaction: String,
67    request_schema: Value,
68    response_schema: Value,
69    domain_error_schema: Value,
70}
71
72fn contract_ir(descriptor: &Descriptor) -> ContractIr {
73    ContractIr {
74        capability_id: descriptor.capability_id.clone(),
75        version: descriptor.version.clone(),
76        portable: descriptor.portable,
77        cross_lane_transfer: descriptor.cross_lane_transfer,
78        operations: descriptor
79            .operations
80            .iter()
81            .map(|operation| OperationIr {
82                name: operation.name.clone(),
83                interaction: operation.interaction.clone(),
84                request: type_ir_from_schema(&operation.request_schema),
85                response: type_ir_from_schema(&operation.response_schema),
86                domain_errors: error_variant_ir_definitions(&operation.domain_error_schema),
87            })
88            .collect(),
89    }
90}
91
92fn type_ir_from_schema(schema: &Value) -> TypeIr {
93    if let Some(types) = schema.get("type").and_then(Value::as_array) {
94        let non_null_types = types
95            .iter()
96            .filter(|schema_type| *schema_type != "null")
97            .cloned()
98            .collect::<Vec<_>>();
99        if non_null_types.is_empty() {
100            return TypeIr::Null;
101        }
102        if non_null_types.len() == 1 {
103            let mut narrowed = schema.as_object().cloned().unwrap_or_default();
104            narrowed.insert("type".to_owned(), non_null_types[0].clone());
105            let base = type_ir_non_null(&Value::Object(narrowed));
106            return if non_null_types.len() == types.len() {
107                base
108            } else {
109                TypeIr::Nullable(Box::new(base))
110            };
111        }
112    }
113    type_ir_non_null(schema)
114}
115
116fn type_ir_non_null(schema: &Value) -> TypeIr {
117    let Some(object) = schema.as_object() else {
118        return TypeIr::Any;
119    };
120    if let Some(values) = object.get("enum").and_then(Value::as_array) {
121        return TypeIr::Enum(
122            values
123                .iter()
124                .filter_map(Value::as_str)
125                .map(ToOwned::to_owned)
126                .collect(),
127        );
128    }
129    if let Some(schema_type) = object.get("type").and_then(Value::as_str) {
130        return match schema_type {
131            "object" => {
132                let required = required_fields(schema);
133                let mut properties = object
134                    .get("properties")
135                    .and_then(Value::as_object)
136                    .into_iter()
137                    .flatten()
138                    .collect::<Vec<_>>();
139                // Projection order is intentionally independent from JSON map
140                // insertion order. Source authoring preserves declaration order
141                // for locked snapshots; language backends retain the historical
142                // canonical name order.
143                properties.sort_unstable_by_key(|(name, _)| *name);
144                let fields = properties
145                    .into_iter()
146                    .map(|(name, schema)| FieldIr {
147                        name: name.clone(),
148                        required: required.contains(name),
149                        sensitive: schema
150                            .get("x-lenso-sensitive")
151                            .and_then(Value::as_bool)
152                            .unwrap_or(false),
153                        ty: type_ir_from_schema(schema),
154                    })
155                    .collect();
156                let additional = match object.get("additionalProperties") {
157                    Some(Value::Bool(false)) => ObjectAdditionalIr::Closed,
158                    Some(Value::Bool(true)) | None => ObjectAdditionalIr::Any,
159                    Some(schema) => {
160                        ObjectAdditionalIr::Typed(Box::new(type_ir_from_schema(schema)))
161                    }
162                };
163                TypeIr::Object { fields, additional }
164            }
165            "array" => object.get("items").map_or(TypeIr::Any, |items| {
166                TypeIr::Array(Box::new(type_ir_from_schema(items)))
167            }),
168            "string" => match object.get("format").and_then(Value::as_str) {
169                Some("int64") => TypeIr::Int64,
170                Some("uint64") => TypeIr::Uint64,
171                Some("byte") => TypeIr::Bytes,
172                Some("date-time") => TypeIr::Timestamp,
173                Some("duration") => TypeIr::Duration,
174                _ => TypeIr::String,
175            },
176            "integer" => TypeIr::Integer,
177            "number" => TypeIr::Number,
178            "boolean" => TypeIr::Boolean,
179            "null" => TypeIr::Null,
180            _ => TypeIr::Any,
181        };
182    }
183    TypeIr::Any
184}
185
186/// A validated, self-contained portable Capability Descriptor.
187#[derive(Clone, Debug)]
188pub struct Descriptor {
189    capability_id: String,
190    capability_major: u64,
191    version: String,
192    parsed_version: Version,
193    portable: bool,
194    cross_lane_transfer: bool,
195    operations: Vec<Operation>,
196}
197
198impl Descriptor {
199    /// Returns the stable Capability series identity, including its major.
200    #[must_use]
201    pub fn capability_id(&self) -> &str {
202        &self.capability_id
203    }
204
205    /// Returns the major encoded in the Capability identity.
206    #[must_use]
207    pub const fn capability_major(&self) -> u64 {
208        self.capability_major
209    }
210
211    /// Returns the exact Descriptor `SemVer` selected for generation.
212    #[must_use]
213    pub fn version(&self) -> &str {
214        &self.version
215    }
216
217    /// Returns whether the Capability is intended to cross Runtime Adapters.
218    #[must_use]
219    pub const fn portable(&self) -> bool {
220        self.portable
221    }
222
223    /// Returns whether generated native values support transfer across Execution Lanes.
224    #[must_use]
225    pub const fn cross_lane_transfer(&self) -> bool {
226        self.cross_lane_transfer
227    }
228
229    /// Returns the stable Operation names in deterministic lexical order.
230    #[must_use]
231    pub fn operation_names(&self) -> Vec<&str> {
232        self.operations
233            .iter()
234            .map(|operation| operation.name.as_str())
235            .collect()
236    }
237}
238
239/// Metadata embedded in every generated language artifact.
240#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct GeneratedMetadata {
242    /// Stable `namespace.name@major` identity.
243    pub capability_id: String,
244    /// Exact Descriptor `SemVer`.
245    pub descriptor_version: String,
246    /// Whether the source Descriptor is portable.
247    pub portable: bool,
248    /// Whether generated native values may transfer across Execution Lanes.
249    pub cross_lane_transfer: bool,
250}
251
252/// One language projection emitted from a portable Capability contract.
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254pub enum ProjectionLanguage {
255    /// Native Rust value, consumer, and provider bindings.
256    Rust,
257    /// TypeScript value, consumer, and provider bindings.
258    TypeScript,
259    /// WebAssembly Component Model WIT provider and consumer worlds.
260    Wit,
261    /// Rust bindings plus the generated byte-Adapter Capability codec.
262    RustRuntime,
263}
264
265/// One independently generated language projection.
266#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct GeneratedProjection {
268    /// Metadata embedded in the generated projection.
269    pub metadata: GeneratedMetadata,
270    /// Selected target language.
271    pub language: ProjectionLanguage,
272    /// Generated provider, client, value, and error bindings.
273    pub source: String,
274}
275
276/// Compatibility aggregate for callers that intentionally generate both projections.
277#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct GeneratedArtifacts {
279    /// Metadata shared by all generated languages.
280    pub metadata: GeneratedMetadata,
281    /// Rust provider, client, value, and error bindings.
282    pub rust: String,
283    /// TypeScript provider, client, value, and error bindings.
284    pub typescript: String,
285}
286
287/// Errors produced while parsing, validating, or generating a contract.
288#[derive(Debug)]
289pub enum CodegenError {
290    /// A file could not be read.
291    Io {
292        path: PathBuf,
293        source: std::io::Error,
294    },
295    /// Descriptor or Schema data is malformed.
296    InvalidDescriptor { detail: String },
297    /// A Schema cannot be used by the portable value profile.
298    UnsupportedSchema { path: PathBuf, detail: String },
299    /// A Descriptor interaction is not supported by the generated language bindings.
300    UnsupportedInteraction {
301        operation: String,
302        interaction: String,
303    },
304    /// A portable JSON Schema shape has no exact Component Model representation.
305    UnsupportedWit { detail: String },
306    /// A JSON value would be lossy when represented by JavaScript numbers.
307    InvalidPortableValue { path: String, detail: String },
308    /// A checked-in generated artifact differs from its source.
309    GeneratedArtifactDrift { path: PathBuf },
310}
311
312impl fmt::Display for CodegenError {
313    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
316            Self::InvalidDescriptor { detail } => {
317                write!(formatter, "invalid Descriptor: {detail}")
318            }
319            Self::UnsupportedSchema { path, detail } => {
320                write!(formatter, "unsupported Schema {}: {detail}", path.display())
321            }
322            Self::UnsupportedInteraction {
323                operation,
324                interaction,
325            } => write!(
326                formatter,
327                "Operation `{operation}` uses unsupported interaction `{interaction}`"
328            ),
329            Self::UnsupportedWit { detail } => {
330                write!(formatter, "unsupported WIT projection: {detail}")
331            }
332            Self::InvalidPortableValue { path, detail } => {
333                write!(formatter, "invalid portable value at `{path}`: {detail}")
334            }
335            Self::GeneratedArtifactDrift { path } => {
336                write!(formatter, "generated artifact is stale: {}", path.display())
337            }
338        }
339    }
340}
341
342impl Error for CodegenError {
343    fn source(&self) -> Option<&(dyn Error + 'static)> {
344        match self {
345            Self::Io { source, .. } => Some(source),
346            _ => None,
347        }
348    }
349}
350
351/// Reasons why a Descriptor evolution is not safe for an existing Capability
352/// series.
353#[derive(Clone, Debug, Eq, PartialEq)]
354pub enum CompatibilityError {
355    /// The two files do not describe the same Capability series.
356    IdentityChanged { from: String, to: String },
357    /// The new Descriptor is not newer than the old one.
358    VersionNotAdvanced { from: String, to: String },
359    /// The change is not allowed by the old series and requires a new major.
360    BreakingChanges { changes: Vec<String> },
361}
362
363impl fmt::Display for CompatibilityError {
364    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
365        match self {
366            Self::IdentityChanged { from, to } => {
367                write!(
368                    formatter,
369                    "Capability identity changed from `{from}` to `{to}`"
370                )
371            }
372            Self::VersionNotAdvanced { from, to } => {
373                write!(
374                    formatter,
375                    "Descriptor version must advance from `{from}` to `{to}`"
376                )
377            }
378            Self::BreakingChanges { changes } => write!(
379                formatter,
380                "breaking Descriptor changes require a new major: {}",
381                changes.join("; ")
382            ),
383        }
384    }
385}
386
387impl Error for CompatibilityError {}
388
389/// Reads and validates one Descriptor, resolving package-local `$ref` files.
390#[allow(clippy::too_many_lines)]
391pub fn load_descriptor(path: &Path) -> Result<Descriptor, CodegenError> {
392    let descriptor_path = canonical_path(path)?;
393    let package_root = descriptor_path
394        .parent()
395        .unwrap_or_else(|| Path::new("."))
396        .to_path_buf();
397    let descriptor_value = read_json(&descriptor_path)?;
398    let object = descriptor_value
399        .as_object()
400        .ok_or_else(|| CodegenError::InvalidDescriptor {
401            detail: "the Descriptor root must be an object".to_owned(),
402        })?;
403
404    let capability_id = required_string(object, "id")?;
405    let (identity, identity_major) = capability_identity(&capability_id)?;
406    let version = required_string(object, "version")?;
407    let parsed_version = Version::parse(&version)?;
408    let portable = object
409        .get("portable")
410        .and_then(Value::as_bool)
411        .unwrap_or(false);
412    let cross_lane_transfer = object
413        .get("cross_lane_transfer")
414        .and_then(Value::as_bool)
415        .unwrap_or(false);
416    let operation_values = object
417        .get("operations")
418        .and_then(Value::as_array)
419        .ok_or_else(|| CodegenError::InvalidDescriptor {
420            detail: "`operations` must be an array".to_owned(),
421        })?;
422    if operation_values.is_empty() {
423        return Err(CodegenError::InvalidDescriptor {
424            detail: "a Descriptor must declare at least one Operation".to_owned(),
425        });
426    }
427
428    let mut operations = Vec::with_capacity(operation_values.len());
429    let mut operation_names = BTreeSet::new();
430    let mut generated_operation_names = BTreeSet::new();
431    let mut generated_type_names = BTreeSet::new();
432    let mut generated_client_method_names =
433        BTreeSet::from(["new".to_owned(), "from_dependencies".to_owned()]);
434    let capability_name = identity
435        .rsplit('.')
436        .next()
437        .map_or_else(|| "Capability".to_owned(), pascal_case);
438    for operation_value in operation_values {
439        let operation =
440            operation_value
441                .as_object()
442                .ok_or_else(|| CodegenError::InvalidDescriptor {
443                    detail: "each Operation must be an object".to_owned(),
444                })?;
445        let name = required_string(operation, "name")?;
446        if !is_identifier(&name) || !is_rust_member_name(&name) {
447            return Err(CodegenError::InvalidDescriptor {
448                detail: format!(
449                    "Operation name `{name}` must start with a letter or `_` and contain only letters, digits, `_`, or `-`"
450                ),
451            });
452        }
453        if !operation_names.insert(name.clone()) {
454            return Err(CodegenError::InvalidDescriptor {
455                detail: format!("Operation `{name}` is declared more than once"),
456            });
457        }
458        for generated_name in [rust_field_name(&name), pascal_case(&name)] {
459            if !generated_operation_names.insert(generated_name.clone()) {
460                return Err(CodegenError::InvalidDescriptor {
461                    detail: format!(
462                        "Operation name `{name}` collides after code generation as `{generated_name}`"
463                    ),
464                });
465            }
466        }
467        let operation_name = pascal_case(&name);
468        let mut generated_names = vec![
469            format!("{operation_name}Request"),
470            format!("{operation_name}Response"),
471            format!("{operation_name}Error"),
472            format!("{operation_name}InvocationError"),
473            format!("{operation_name}Result"),
474            format!("{capability_name}{operation_name}"),
475        ];
476        generated_names.push(if operation_values.len() == 1 {
477            format!("{capability_name}InvocationError")
478        } else {
479            format!("{capability_name}{operation_name}InvocationError")
480        });
481        for generated_name in generated_names {
482            if !is_generated_type_name(&generated_name)
483                || !generated_type_names.insert(generated_name.clone())
484            {
485                return Err(CodegenError::InvalidDescriptor {
486                    detail: format!(
487                        "Operation name `{name}` collides after type generation as `{generated_name}`"
488                    ),
489                });
490            }
491        }
492        let interaction = required_string(operation, "interaction")?;
493        if !matches!(interaction.as_str(), "request" | "stream" | "event") {
494            return Err(CodegenError::InvalidDescriptor {
495                detail: format!("Operation `{name}` has unsupported interaction `{interaction}`"),
496            });
497        }
498        if matches!(interaction.as_str(), "request" | "stream" | "event") {
499            let client_method_name = rust_field_name(&name);
500            for generated_name in [
501                client_method_name.clone(),
502                format!("{client_method_name}_with_context"),
503            ] {
504                if !generated_client_method_names.insert(generated_name.clone()) {
505                    return Err(CodegenError::InvalidDescriptor {
506                        detail: format!(
507                            "Operation name `{name}` collides with the generated Client API as `{generated_name}`"
508                        ),
509                    });
510                }
511            }
512        }
513        let request_schema_path =
514            schema_path(&descriptor_path, operation, "request_schema", &name)?;
515        let response_schema_path =
516            schema_path(&descriptor_path, operation, "response_schema", &name)?;
517        let domain_error_schema_path =
518            schema_path(&descriptor_path, operation, "domain_error_schema", &name)?;
519        let mut ref_stack = Vec::new();
520        let request_source = read_json(&request_schema_path)?;
521        let request_schema = resolve_refs(
522            &request_source,
523            &request_source,
524            &request_schema_path,
525            &package_root,
526            &mut ref_stack,
527        )?;
528        validate_schema_profile(&request_schema, &request_schema_path)?;
529        validate_value_generation_schema(&request_schema, &request_schema_path)?;
530        let mut ref_stack = Vec::new();
531        let response_source = read_json(&response_schema_path)?;
532        let response_schema = resolve_refs(
533            &response_source,
534            &response_source,
535            &response_schema_path,
536            &package_root,
537            &mut ref_stack,
538        )?;
539        validate_schema_profile(&response_schema, &response_schema_path)?;
540        validate_value_generation_schema(&response_schema, &response_schema_path)?;
541        let mut ref_stack = Vec::new();
542        let domain_error_source = read_json(&domain_error_schema_path)?;
543        let domain_error_schema = resolve_refs(
544            &domain_error_source,
545            &domain_error_source,
546            &domain_error_schema_path,
547            &package_root,
548            &mut ref_stack,
549        )?;
550        validate_schema_profile(&domain_error_schema, &domain_error_schema_path)?;
551        validate_domain_error_schema(&domain_error_schema, &domain_error_schema_path)?;
552        operations.push(Operation {
553            name,
554            interaction,
555            request_schema,
556            response_schema,
557            domain_error_schema,
558        });
559    }
560    operations.sort_by(|left, right| left.name.cmp(&right.name));
561
562    Ok(Descriptor {
563        capability_id: format!("{identity}@{identity_major}"),
564        capability_major: identity_major,
565        version,
566        parsed_version,
567        portable,
568        cross_lane_transfer,
569        operations,
570    })
571}
572
573fn required_string(object: &Map<String, Value>, key: &str) -> Result<String, CodegenError> {
574    object
575        .get(key)
576        .and_then(Value::as_str)
577        .filter(|value| !value.is_empty())
578        .map(ToOwned::to_owned)
579        .ok_or_else(|| CodegenError::InvalidDescriptor {
580            detail: format!("`{key}` must be a non-empty string"),
581        })
582}
583
584fn capability_identity(value: &str) -> Result<(String, u64), CodegenError> {
585    let Some((identity, major)) = value.rsplit_once('@') else {
586        return Err(CodegenError::InvalidDescriptor {
587            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
588        });
589    };
590    let generated_name = identity
591        .rsplit('.')
592        .next()
593        .map(pascal_case)
594        .unwrap_or_default();
595    if identity.is_empty()
596        || identity.split('.').count() < 2
597        || !identity.split('.').all(is_identifier)
598        || !is_generated_type_name(&generated_name)
599    {
600        return Err(CodegenError::InvalidDescriptor {
601            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
602        });
603    }
604    let Some(major) = parse_numeric_component(major) else {
605        return Err(CodegenError::InvalidDescriptor {
606            detail: format!("Capability id `{value}` has an invalid major"),
607        });
608    };
609    Ok((identity.to_owned(), major))
610}
611
612fn schema_path(
613    descriptor_path: &Path,
614    operation: &Map<String, Value>,
615    key: &str,
616    operation_name: &str,
617) -> Result<PathBuf, CodegenError> {
618    let value = required_string(operation, key).map_err(|error| match error {
619        CodegenError::InvalidDescriptor { detail } => CodegenError::InvalidDescriptor {
620            detail: format!("Operation `{operation_name}`: {detail}"),
621        },
622        other => other,
623    })?;
624    let candidate = descriptor_path
625        .parent()
626        .unwrap_or_else(|| Path::new("."))
627        .join(value);
628    let canonical = canonical_path(&candidate)?;
629    let package_root = canonical_path(descriptor_path.parent().unwrap_or_else(|| Path::new(".")))?;
630    if !canonical.starts_with(&package_root) {
631        return Err(CodegenError::UnsupportedSchema {
632            path: canonical,
633            detail: "Schema paths must remain inside the Descriptor package".to_owned(),
634        });
635    }
636    Ok(canonical)
637}
638
639fn canonical_path(path: &Path) -> Result<PathBuf, CodegenError> {
640    fs::canonicalize(path).map_err(|source| CodegenError::Io {
641        path: path.to_path_buf(),
642        source,
643    })
644}
645
646fn read_json(path: &Path) -> Result<Value, CodegenError> {
647    let source = fs::read_to_string(path).map_err(|source| CodegenError::Io {
648        path: path.to_path_buf(),
649        source,
650    })?;
651    serde_json::from_str(&source).map_err(|error| CodegenError::InvalidDescriptor {
652        detail: format!("{}: {error}", path.display()),
653    })
654}
655
656fn resolve_refs(
657    value: &Value,
658    root: &Value,
659    source_path: &Path,
660    package_root: &Path,
661    stack: &mut Vec<(PathBuf, String)>,
662) -> Result<Value, CodegenError> {
663    match value {
664        Value::Object(object) => {
665            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
666                let target_value = if reference.starts_with('#') {
667                    let fragment = reference.strip_prefix('#').unwrap_or_default();
668                    let target_path = source_path.to_path_buf();
669                    let key = (target_path.clone(), reference.to_owned());
670                    if stack.contains(&key) {
671                        return Err(CodegenError::UnsupportedSchema {
672                            path: source_path.to_path_buf(),
673                            detail: format!("cyclic local Schema `$ref` `{reference}`"),
674                        });
675                    }
676                    let target = json_pointer(root, fragment).ok_or_else(|| {
677                        CodegenError::UnsupportedSchema {
678                            path: source_path.to_path_buf(),
679                            detail: format!(
680                                "local JSON Pointer `$ref` `{reference}` was not found"
681                            ),
682                        }
683                    })?;
684                    stack.push(key);
685                    let resolved = resolve_refs(target, root, source_path, package_root, stack)?;
686                    stack.pop();
687                    resolved
688                } else {
689                    let (reference_path, fragment) =
690                        reference.split_once('#').unwrap_or((reference, ""));
691                    let target_path = source_path
692                        .parent()
693                        .unwrap_or_else(|| Path::new("."))
694                        .join(reference_path);
695                    let target_path = canonical_path(&target_path)?;
696                    if !target_path.starts_with(package_root) {
697                        return Err(CodegenError::UnsupportedSchema {
698                            path: source_path.to_path_buf(),
699                            detail: format!(
700                                "external Schema `$ref` `{reference}` leaves the Descriptor package"
701                            ),
702                        });
703                    }
704                    let key = (target_path.clone(), format!("#{fragment}"));
705                    if stack.contains(&key) {
706                        return Err(CodegenError::UnsupportedSchema {
707                            path: source_path.to_path_buf(),
708                            detail: format!(
709                                "cyclic Schema `$ref` through {}",
710                                target_path.display()
711                            ),
712                        });
713                    }
714                    let target_root = read_json(&target_path)?;
715                    let target = json_pointer(&target_root, fragment).ok_or_else(|| {
716                        CodegenError::UnsupportedSchema {
717                            path: source_path.to_path_buf(),
718                            detail: format!(
719                                "external JSON Pointer `$ref` `{reference}` was not found"
720                            ),
721                        }
722                    })?;
723                    stack.push(key);
724                    let resolved =
725                        resolve_refs(target, &target_root, &target_path, package_root, stack)?;
726                    stack.pop();
727                    resolved
728                };
729                let mut merged = target_value.as_object().cloned().ok_or_else(|| {
730                    CodegenError::UnsupportedSchema {
731                        path: source_path.to_path_buf(),
732                        detail: "a `$ref` target must resolve to an object Schema".to_owned(),
733                    }
734                })?;
735                for (key, child) in object {
736                    if key != "$ref" {
737                        merged.insert(
738                            key.clone(),
739                            resolve_refs(child, root, source_path, package_root, stack)?,
740                        );
741                    }
742                }
743                return Ok(Value::Object(merged));
744            }
745            let mut resolved = Map::new();
746            for (key, child) in object {
747                resolved.insert(
748                    key.clone(),
749                    resolve_refs(child, root, source_path, package_root, stack)?,
750                );
751            }
752            Ok(Value::Object(resolved))
753        }
754        Value::Array(values) => values
755            .iter()
756            .map(|child| resolve_refs(child, root, source_path, package_root, stack))
757            .collect::<Result<Vec<_>, _>>()
758            .map(Value::Array),
759        _ => Ok(value.clone()),
760    }
761}
762
763fn json_pointer<'a>(root: &'a Value, fragment: &str) -> Option<&'a Value> {
764    if fragment.is_empty() {
765        return Some(root);
766    }
767    let pointer = fragment.strip_prefix('/')?;
768    let mut current = root;
769    for token in pointer.split('/') {
770        let token = token.replace("~1", "/").replace("~0", "~");
771        current = match current {
772            Value::Object(object) => object.get(&token)?,
773            Value::Array(array) => array.get(token.parse::<usize>().ok()?)?,
774            _ => return None,
775        };
776    }
777    Some(current)
778}
779
780fn validate_schema_profile(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
781    let Some(object) = schema.as_object() else {
782        return Err(CodegenError::UnsupportedSchema {
783            path: source_path.to_path_buf(),
784            detail: "a JSON Schema must be an object".to_owned(),
785        });
786    };
787
788    if let Some(format) = object.get("format").and_then(Value::as_str) {
789        if !matches!(
790            format,
791            "int64" | "uint64" | "byte" | "date-time" | "duration"
792        ) {
793            return Err(CodegenError::UnsupportedSchema {
794                path: source_path.to_path_buf(),
795                detail: format!("format `{format}` is outside the portable value profile"),
796            });
797        }
798        if !schema_includes_type(object, "string") {
799            return Err(CodegenError::UnsupportedSchema {
800                path: source_path.to_path_buf(),
801                detail: format!("portable format `{format}` must be attached to a string Schema"),
802            });
803        }
804    }
805
806    if let Some(schema_type) = object.get("type") {
807        match schema_type {
808            Value::String(schema_type) => validate_schema_type(schema_type, object, source_path)?,
809            Value::Array(types) => {
810                if types.is_empty() {
811                    return Err(CodegenError::UnsupportedSchema {
812                        path: source_path.to_path_buf(),
813                        detail: "a Schema type union cannot be empty".to_owned(),
814                    });
815                }
816                for schema_type in types {
817                    let Some(schema_type) = schema_type.as_str() else {
818                        return Err(CodegenError::UnsupportedSchema {
819                            path: source_path.to_path_buf(),
820                            detail: "Schema type unions must contain strings".to_owned(),
821                        });
822                    };
823                    validate_schema_type(schema_type, object, source_path)?;
824                }
825            }
826            _ => {
827                return Err(CodegenError::UnsupportedSchema {
828                    path: source_path.to_path_buf(),
829                    detail: "Schema `type` must be a string or array".to_owned(),
830                });
831            }
832        }
833    } else if let Some(alternatives) = object
834        .get("oneOf")
835        .or_else(|| object.get("anyOf"))
836        .and_then(Value::as_array)
837    {
838        for alternative in alternatives {
839            validate_schema_profile(alternative, source_path)?;
840        }
841    } else if !object.contains_key("const") && !object.contains_key("enum") {
842        return Err(CodegenError::UnsupportedSchema {
843            path: source_path.to_path_buf(),
844            detail: "Schema needs a supported `type`, `oneOf`, `anyOf`, `const`, or `enum`"
845                .to_owned(),
846        });
847    }
848
849    if let Some(properties) = object.get("properties") {
850        let Some(properties) = properties.as_object() else {
851            return Err(CodegenError::UnsupportedSchema {
852                path: source_path.to_path_buf(),
853                detail: "Schema `properties` must be an object".to_owned(),
854            });
855        };
856        for property in properties.values() {
857            validate_schema_profile(property, source_path)?;
858        }
859    }
860    if let Some(items) = object.get("items") {
861        validate_schema_profile(items, source_path)?;
862    }
863    if let Some(additional) = object.get("additionalProperties")
864        && !additional.is_boolean()
865    {
866        validate_schema_profile(additional, source_path)?;
867    }
868    if let Some(definitions) = object.get("$defs").and_then(Value::as_object) {
869        for definition in definitions.values() {
870            validate_schema_profile(definition, source_path)?;
871        }
872    }
873    if let Some(alternatives) = object
874        .get("oneOf")
875        .or_else(|| object.get("anyOf"))
876        .and_then(Value::as_array)
877    {
878        for alternative in alternatives {
879            validate_schema_profile(alternative, source_path)?;
880        }
881    }
882    Ok(())
883}
884
885fn validate_schema_type(
886    schema_type: &str,
887    schema: &Map<String, Value>,
888    source_path: &Path,
889) -> Result<(), CodegenError> {
890    if !matches!(
891        schema_type,
892        "object" | "array" | "string" | "integer" | "number" | "boolean" | "null"
893    ) {
894        return Err(CodegenError::UnsupportedSchema {
895            path: source_path.to_path_buf(),
896            detail: format!("Schema type `{schema_type}` is outside the portable profile"),
897        });
898    }
899    if schema_type == "array" && !schema.contains_key("items") {
900        return Err(CodegenError::UnsupportedSchema {
901            path: source_path.to_path_buf(),
902            detail: "array Schemas must declare `items`".to_owned(),
903        });
904    }
905    Ok(())
906}
907
908fn schema_includes_type(schema: &Map<String, Value>, expected: &str) -> bool {
909    match schema.get("type") {
910        Some(Value::String(schema_type)) => schema_type == expected,
911        Some(Value::Array(types)) => types.iter().any(|schema_type| {
912            schema_type
913                .as_str()
914                .is_some_and(|schema_type| schema_type == expected)
915        }),
916        _ => false,
917    }
918}
919
920#[allow(clippy::too_many_lines)]
921fn validate_value_generation_schema(
922    schema: &Value,
923    source_path: &Path,
924) -> Result<(), CodegenError> {
925    let Some(object) = schema.as_object() else {
926        return Err(CodegenError::UnsupportedSchema {
927            path: source_path.to_path_buf(),
928            detail: "generated value Schemas must be objects".to_owned(),
929        });
930    };
931    if object.contains_key("oneOf") || object.contains_key("anyOf") {
932        return Err(CodegenError::UnsupportedSchema {
933            path: source_path.to_path_buf(),
934            detail: "generated value Schemas do not support oneOf/anyOf unions".to_owned(),
935        });
936    }
937    if object.contains_key("allOf") {
938        return Err(CodegenError::UnsupportedSchema {
939            path: source_path.to_path_buf(),
940            detail: "generated value Schemas do not support allOf unions".to_owned(),
941        });
942    }
943    if object.contains_key("const") {
944        return Err(CodegenError::UnsupportedSchema {
945            path: source_path.to_path_buf(),
946            detail: "generated value Schemas do not support const fields".to_owned(),
947        });
948    }
949    if let Some(types) = object.get("type").and_then(Value::as_array) {
950        let non_null = types
951            .iter()
952            .filter(|schema_type| *schema_type != "null")
953            .count();
954        if non_null > 1 {
955            return Err(CodegenError::UnsupportedSchema {
956                path: source_path.to_path_buf(),
957                detail: "generated value Schemas only support nullable type unions".to_owned(),
958            });
959        }
960        let mut narrowed = object.clone();
961        let Some(schema_type) = types.iter().find(|schema_type| *schema_type != "null") else {
962            return Ok(());
963        };
964        narrowed.insert("type".to_owned(), schema_type.clone());
965        return validate_value_generation_schema(&Value::Object(narrowed), source_path);
966    }
967    if let Some(values) = object.get("enum").and_then(Value::as_array) {
968        if values.is_empty() || !values.iter().all(Value::is_string) {
969            return Err(CodegenError::UnsupportedSchema {
970                path: source_path.to_path_buf(),
971                detail: "generated enum values must be non-empty strings".to_owned(),
972            });
973        }
974        let mut enum_names = BTreeSet::new();
975        for value in values.iter().filter_map(Value::as_str) {
976            let name = pascal_case(value);
977            if !is_generated_enum_variant(&name) || !enum_names.insert(name) {
978                return Err(CodegenError::UnsupportedSchema {
979                    path: source_path.to_path_buf(),
980                    detail: "generated enum values collide after Rust variant generation"
981                        .to_owned(),
982                });
983            }
984        }
985    }
986    let Some(schema_type) = object.get("type").and_then(Value::as_str) else {
987        if object.contains_key("enum") {
988            return Ok(());
989        }
990        return Err(CodegenError::UnsupportedSchema {
991            path: source_path.to_path_buf(),
992            detail: "generated value Schemas need a supported type or string enum".to_owned(),
993        });
994    };
995    match schema_type {
996        "object" => {
997            let properties = object
998                .get("properties")
999                .and_then(Value::as_object)
1000                .cloned()
1001                .unwrap_or_default();
1002            let mut rust_names = BTreeSet::new();
1003            let mut nested_type_names = BTreeSet::new();
1004            for (name, property) in &properties {
1005                if !rust_names.insert(rust_field_name(name)) {
1006                    return Err(CodegenError::UnsupportedSchema {
1007                        path: source_path.to_path_buf(),
1008                        detail: format!(
1009                            "object properties collide after Rust field normalization near `{name}`"
1010                        ),
1011                    });
1012                }
1013                if !nested_type_names.insert(pascal_case(name)) {
1014                    return Err(CodegenError::UnsupportedSchema {
1015                        path: source_path.to_path_buf(),
1016                        detail: format!(
1017                            "object properties collide after nested type-name generation near `{name}`"
1018                        ),
1019                    });
1020                }
1021                validate_value_generation_schema(property, source_path)?;
1022            }
1023            match object.get("additionalProperties") {
1024                Some(Value::Bool(false)) => {}
1025                Some(Value::Bool(true)) | None if !properties.is_empty() => {
1026                    return Err(CodegenError::UnsupportedSchema {
1027                        path: source_path.to_path_buf(),
1028                        detail: "open objects with declared properties would lose unknown fields"
1029                            .to_owned(),
1030                    });
1031                }
1032                Some(additional) if !additional.is_boolean() && !properties.is_empty() => {
1033                    return Err(CodegenError::UnsupportedSchema {
1034                        path: source_path.to_path_buf(),
1035                        detail: "objects cannot combine declared properties with a map payload"
1036                            .to_owned(),
1037                    });
1038                }
1039                Some(additional) if !additional.is_boolean() => {
1040                    validate_value_generation_schema(additional, source_path)?;
1041                }
1042                _ => {}
1043            }
1044        }
1045        "array" => {
1046            let items = object
1047                .get("items")
1048                .ok_or_else(|| CodegenError::UnsupportedSchema {
1049                    path: source_path.to_path_buf(),
1050                    detail: "generated array Schemas must declare items".to_owned(),
1051                })?;
1052            validate_value_generation_schema(items, source_path)?;
1053        }
1054        "string" | "integer" | "number" | "boolean" | "null" => {}
1055        _ => {
1056            return Err(CodegenError::UnsupportedSchema {
1057                path: source_path.to_path_buf(),
1058                detail: format!("Schema type `{schema_type}` cannot be generated"),
1059            });
1060        }
1061    }
1062    Ok(())
1063}
1064
1065fn validate_domain_error_schema(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1066    if schema
1067        .as_object()
1068        .is_some_and(|object| object.contains_key("allOf"))
1069    {
1070        return Err(CodegenError::UnsupportedSchema {
1071            path: source_path.to_path_buf(),
1072            detail: "Domain Error Schemas do not support allOf unions".to_owned(),
1073        });
1074    }
1075    let variants = schema
1076        .get("oneOf")
1077        .and_then(Value::as_array)
1078        .filter(|variants| !variants.is_empty())
1079        .ok_or_else(|| CodegenError::UnsupportedSchema {
1080            path: source_path.to_path_buf(),
1081            detail: "Domain Error Schemas must be a non-empty oneOf union".to_owned(),
1082        })?;
1083    let mut codes = BTreeSet::new();
1084    let mut names = BTreeSet::from(["Unknown".to_owned()]);
1085    for variant in variants {
1086        if variant
1087            .as_object()
1088            .is_some_and(|object| object.contains_key("allOf"))
1089        {
1090            return Err(CodegenError::UnsupportedSchema {
1091                path: source_path.to_path_buf(),
1092                detail: "Domain Error variants do not support allOf unions".to_owned(),
1093            });
1094        }
1095        if let Some(code) = variant.get("const").and_then(Value::as_str) {
1096            let name = pascal_case(code);
1097            if !codes.insert(code.to_owned())
1098                || !names.insert(name.clone())
1099                || !is_generated_enum_variant(&name)
1100            {
1101                return Err(CodegenError::UnsupportedSchema {
1102                    path: source_path.to_path_buf(),
1103                    detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1104                });
1105            }
1106            continue;
1107        }
1108        let object = variant
1109            .as_object()
1110            .ok_or_else(|| CodegenError::UnsupportedSchema {
1111                path: source_path.to_path_buf(),
1112                detail: "Domain Error variants must be string consts or objects".to_owned(),
1113            })?;
1114        let properties = object
1115            .get("properties")
1116            .and_then(Value::as_object)
1117            .ok_or_else(|| CodegenError::UnsupportedSchema {
1118                path: source_path.to_path_buf(),
1119                detail: "structured Domain Error variants need properties".to_owned(),
1120            })?;
1121        if object.get("additionalProperties") != Some(&Value::Bool(false))
1122            || !required_fields(variant).contains("code")
1123            || properties
1124                .keys()
1125                .any(|name| name != "code" && name != "payload")
1126        {
1127            return Err(CodegenError::UnsupportedSchema {
1128                path: source_path.to_path_buf(),
1129                detail:
1130                    "structured Domain Error objects must explicitly allow only code and payload"
1131                        .to_owned(),
1132            });
1133        }
1134        let code = object
1135            .get("properties")
1136            .and_then(Value::as_object)
1137            .and_then(|properties| properties.get("code"))
1138            .and_then(|code| code.get("const"))
1139            .and_then(Value::as_str)
1140            .ok_or_else(|| CodegenError::UnsupportedSchema {
1141                path: source_path.to_path_buf(),
1142                detail: "structured Domain Error variants need a const string code".to_owned(),
1143            })?;
1144        let name = pascal_case(code);
1145        if !codes.insert(code.to_owned())
1146            || !names.insert(name.clone())
1147            || !is_generated_enum_variant(&name)
1148        {
1149            return Err(CodegenError::UnsupportedSchema {
1150                path: source_path.to_path_buf(),
1151                detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1152            });
1153        }
1154        if let Some(payload) = object
1155            .get("properties")
1156            .and_then(Value::as_object)
1157            .and_then(|properties| properties.get("payload"))
1158        {
1159            validate_value_generation_schema(payload, source_path)?;
1160        }
1161    }
1162    Ok(())
1163}
1164
1165fn generation_input(path: &Path) -> Result<(GeneratedMetadata, ContractIr), CodegenError> {
1166    let descriptor = load_descriptor(path)?;
1167    let contract = contract_ir(&descriptor);
1168    let metadata = GeneratedMetadata {
1169        capability_id: descriptor.capability_id.clone(),
1170        descriptor_version: descriptor.version.clone(),
1171        portable: descriptor.portable,
1172        cross_lane_transfer: descriptor.cross_lane_transfer,
1173    };
1174    Ok((metadata, contract))
1175}
1176
1177/// Generates one selected language projection from a Descriptor source.
1178pub fn generate_projection(
1179    path: &Path,
1180    language: ProjectionLanguage,
1181) -> Result<GeneratedProjection, CodegenError> {
1182    let (metadata, contract) = generation_input(path)?;
1183    let source = match language {
1184        ProjectionLanguage::Rust => generate_rust(&contract),
1185        ProjectionLanguage::TypeScript => generate_typescript(&contract),
1186        ProjectionLanguage::Wit => wit::generate_wit(&contract)?,
1187        ProjectionLanguage::RustRuntime => generate_rust_runtime(&contract)?,
1188    };
1189    Ok(GeneratedProjection {
1190        metadata,
1191        language,
1192        source,
1193    })
1194}
1195
1196/// Generates both language artifacts from one Descriptor source.
1197pub fn generate(path: &Path) -> Result<GeneratedArtifacts, CodegenError> {
1198    let (metadata, contract) = generation_input(path)?;
1199    Ok(GeneratedArtifacts {
1200        metadata,
1201        rust: generate_rust(&contract),
1202        typescript: generate_typescript(&contract),
1203    })
1204}
1205
1206/// Writes generated artifacts to checked-in paths.
1207pub fn write_generated(
1208    descriptor_path: &Path,
1209    rust_path: &Path,
1210    typescript_path: &Path,
1211) -> Result<(), CodegenError> {
1212    let artifacts = generate(descriptor_path)?;
1213    write_artifact(rust_path, &artifacts.rust)?;
1214    write_artifact(typescript_path, &artifacts.typescript)?;
1215    Ok(())
1216}
1217
1218/// Writes one selected language projection to a checked-in path.
1219pub fn write_projection(
1220    descriptor_path: &Path,
1221    language: ProjectionLanguage,
1222    output_path: &Path,
1223) -> Result<(), CodegenError> {
1224    let projection = generate_projection(descriptor_path, language)?;
1225    write_artifact(output_path, &projection.source)
1226}
1227
1228/// Fails when either checked-in artifact is not exactly reproducible.
1229pub fn check_generated(
1230    descriptor_path: &Path,
1231    rust_path: &Path,
1232    typescript_path: &Path,
1233) -> Result<(), CodegenError> {
1234    let artifacts = generate(descriptor_path)?;
1235    check_artifact(rust_path, &artifacts.rust)?;
1236    check_artifact(typescript_path, &artifacts.typescript)?;
1237    Ok(())
1238}
1239
1240/// Fails when one selected language projection is not exactly reproducible.
1241pub fn check_projection(
1242    descriptor_path: &Path,
1243    language: ProjectionLanguage,
1244    output_path: &Path,
1245) -> Result<(), CodegenError> {
1246    let projection = generate_projection(descriptor_path, language)?;
1247    check_artifact(output_path, &projection.source)
1248}
1249
1250/// Round-trips one JSON value through the portable wire representation.
1251///
1252/// Raw JSON numbers are restricted to JavaScript's safe integer range. Wide
1253/// integers must therefore be represented by the Descriptor's `int64` or
1254/// `uint64` decimal-string formats, which this function preserves exactly.
1255pub fn round_trip_portable_json(value: &Value) -> Result<Value, CodegenError> {
1256    validate_portable_value(value, "$".to_owned())?;
1257    let encoded = serde_json::to_vec(value).expect("JSON values are serializable");
1258    serde_json::from_slice(&encoded).map_err(|error| CodegenError::InvalidPortableValue {
1259        path: "$".to_owned(),
1260        detail: format!("wire JSON could not be decoded: {error}"),
1261    })
1262}
1263
1264/// Validates a wire value against a package-local JSON Schema.
1265///
1266/// This is intentionally a small validator for the Descriptor's portable
1267/// profile rather than a general JSON Schema implementation. It makes the
1268/// profile's loss-sensitive mappings executable: wide integers must use their
1269/// decimal-string formats, bytes must be canonical base64, and timestamp and
1270/// duration strings must have their documented wire shapes.
1271pub fn validate_wire_value(schema_path: &Path, value: &Value) -> Result<(), CodegenError> {
1272    let schema_path = canonical_path(schema_path)?;
1273    let package_root = schema_path
1274        .parent()
1275        .unwrap_or_else(|| Path::new("."))
1276        .to_path_buf();
1277    let source = read_json(&schema_path)?;
1278    let mut ref_stack = Vec::new();
1279    let schema = resolve_refs(
1280        &source,
1281        &source,
1282        &schema_path,
1283        &package_root,
1284        &mut ref_stack,
1285    )?;
1286    validate_schema_profile(&schema, &schema_path)?;
1287    validate_wire_value_inner(&schema, value, "$", &schema_path)
1288}
1289
1290fn validate_wire_value_inner(
1291    schema: &Value,
1292    value: &Value,
1293    path: &str,
1294    source_path: &Path,
1295) -> Result<(), CodegenError> {
1296    if let Some(constant) = schema.get("const")
1297        && value != constant
1298    {
1299        return invalid_wire_value(path, "value does not match the Schema const", source_path);
1300    }
1301    if let Some(values) = schema.get("enum").and_then(Value::as_array)
1302        && !values.iter().any(|candidate| candidate == value)
1303    {
1304        return invalid_wire_value(
1305            path,
1306            "value is not included in the Schema enum",
1307            source_path,
1308        );
1309    }
1310
1311    if let Some(alternatives) = schema.get("oneOf").and_then(Value::as_array) {
1312        let matches = alternatives
1313            .iter()
1314            .filter(|alternative| {
1315                validate_wire_value_inner(alternative, value, path, source_path).is_ok()
1316            })
1317            .count();
1318        if matches == 0
1319            && is_open_domain_error_schema(schema)
1320            && is_unknown_domain_error_wire(schema, value)
1321        {
1322            return validate_portable_value(value, path.to_owned());
1323        }
1324        if matches != 1 {
1325            return invalid_wire_value(
1326                path,
1327                "value must match exactly one Schema oneOf alternative",
1328                source_path,
1329            );
1330        }
1331    }
1332    if let Some(alternatives) = schema.get("anyOf").and_then(Value::as_array)
1333        && !alternatives.iter().any(|alternative| {
1334            validate_wire_value_inner(alternative, value, path, source_path).is_ok()
1335        })
1336    {
1337        return invalid_wire_value(
1338            path,
1339            "value must match at least one Schema anyOf alternative",
1340            source_path,
1341        );
1342    }
1343
1344    match schema.get("type") {
1345        Some(Value::String(schema_type)) => {
1346            validate_wire_type(schema, schema_type, value, path, source_path)
1347        }
1348        Some(Value::Array(types)) => {
1349            let mut errors = Vec::new();
1350            for schema_type in types {
1351                let Some(schema_type) = schema_type.as_str() else {
1352                    continue;
1353                };
1354                match validate_wire_type(schema, schema_type, value, path, source_path) {
1355                    Ok(()) => return Ok(()),
1356                    Err(error) => errors.push(error.to_string()),
1357                }
1358            }
1359            invalid_wire_value(
1360                path,
1361                &format!(
1362                    "value does not match any Schema type ({})",
1363                    errors.join("; ")
1364                ),
1365                source_path,
1366            )
1367        }
1368        Some(_) => invalid_wire_value(path, "Schema type must be a string or array", source_path),
1369        None if schema
1370            .as_object()
1371            .is_some_and(|object| object.contains_key("const") || object.contains_key("enum")) =>
1372        {
1373            Ok(())
1374        }
1375        None => Ok(()),
1376    }
1377}
1378
1379fn is_open_domain_error_schema(schema: &Value) -> bool {
1380    schema
1381        .get("oneOf")
1382        .and_then(Value::as_array)
1383        .is_some_and(|variants| {
1384            !variants.is_empty()
1385                && variants.iter().all(|variant| {
1386                    variant.get("const").and_then(Value::as_str).is_some()
1387                        || variant
1388                            .get("properties")
1389                            .and_then(Value::as_object)
1390                            .and_then(|properties| properties.get("code"))
1391                            .and_then(|code| code.get("const"))
1392                            .and_then(Value::as_str)
1393                            .is_some()
1394                })
1395        })
1396}
1397
1398fn is_unknown_domain_error_wire(schema: &Value, value: &Value) -> bool {
1399    let known_codes = schema
1400        .get("oneOf")
1401        .and_then(Value::as_array)
1402        .into_iter()
1403        .flatten()
1404        .filter_map(|variant| {
1405            variant.get("const").and_then(Value::as_str).or_else(|| {
1406                variant
1407                    .get("properties")
1408                    .and_then(Value::as_object)
1409                    .and_then(|properties| properties.get("code"))
1410                    .and_then(|code| code.get("const"))
1411                    .and_then(Value::as_str)
1412            })
1413        })
1414        .collect::<BTreeSet<_>>();
1415    match value {
1416        Value::String(code) => !known_codes.contains(code.as_str()),
1417        Value::Object(object) => object
1418            .get("code")
1419            .and_then(Value::as_str)
1420            .is_some_and(|code| !known_codes.contains(code)),
1421        _ => false,
1422    }
1423}
1424
1425fn validate_wire_type(
1426    schema: &Value,
1427    schema_type: &str,
1428    value: &Value,
1429    path: &str,
1430    source_path: &Path,
1431) -> Result<(), CodegenError> {
1432    match schema_type {
1433        "object" => validate_wire_object(schema, value, path, source_path),
1434        "array" => validate_wire_array(schema, value, path, source_path),
1435        "string" => validate_wire_string(schema, value, path, source_path),
1436        "integer" => {
1437            if !value.is_number()
1438                || value.as_i64().is_none() && value.as_u64().is_none()
1439                || !is_safe_json_integer(value)
1440            {
1441                return invalid_wire_value(path, "expected an integer", source_path);
1442            }
1443            validate_numeric_constraint(schema, value, path, source_path)
1444        }
1445        "number" => {
1446            if value
1447                .as_number()
1448                .is_none_or(|number| !is_safe_json_number(number))
1449            {
1450                return invalid_wire_value(path, "expected a finite number", source_path);
1451            }
1452            validate_numeric_constraint(schema, value, path, source_path)
1453        }
1454        "boolean" => {
1455            if !value.is_boolean() {
1456                return invalid_wire_value(path, "expected a boolean", source_path);
1457            }
1458            Ok(())
1459        }
1460        "null" => {
1461            if !value.is_null() {
1462                return invalid_wire_value(path, "expected null", source_path);
1463            }
1464            Ok(())
1465        }
1466        _ => invalid_wire_value(
1467            path,
1468            "Schema type is outside the portable profile",
1469            source_path,
1470        ),
1471    }
1472}
1473
1474fn is_safe_json_integer(value: &Value) -> bool {
1475    value
1476        .as_i64()
1477        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
1478        || value
1479            .as_u64()
1480            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
1481}
1482
1483fn is_safe_json_number(number: &serde_json::Number) -> bool {
1484    number
1485        .as_i64()
1486        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
1487        || number
1488            .as_u64()
1489            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
1490        || number.as_f64().is_some_and(|value| {
1491            value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
1492        })
1493}
1494
1495fn validate_wire_object(
1496    schema: &Value,
1497    value: &Value,
1498    path: &str,
1499    source_path: &Path,
1500) -> Result<(), CodegenError> {
1501    let Value::Object(properties) = value else {
1502        return invalid_wire_value(path, "expected an object", source_path);
1503    };
1504    let required = required_fields(schema);
1505    for field in required {
1506        if !properties.contains_key(&field) {
1507            return invalid_wire_value(
1508                &format!("{path}.{field}"),
1509                "required field is missing",
1510                source_path,
1511            );
1512        }
1513    }
1514    let declared = schema
1515        .get("properties")
1516        .and_then(Value::as_object)
1517        .cloned()
1518        .unwrap_or_default();
1519    for (field, field_value) in properties {
1520        if let Some(field_schema) = declared.get(field) {
1521            validate_wire_value_inner(
1522                field_schema,
1523                field_value,
1524                &format!("{path}.{field}"),
1525                source_path,
1526            )?;
1527            continue;
1528        }
1529        match schema.get("additionalProperties") {
1530            Some(Value::Bool(false)) => {
1531                return invalid_wire_value(
1532                    &format!("{path}.{field}"),
1533                    "additional property is not allowed",
1534                    source_path,
1535                );
1536            }
1537            Some(additional_schema) if !additional_schema.is_boolean() => {
1538                validate_wire_value_inner(
1539                    additional_schema,
1540                    field_value,
1541                    &format!("{path}.{field}"),
1542                    source_path,
1543                )?;
1544            }
1545            _ => {}
1546        }
1547    }
1548    Ok(())
1549}
1550
1551fn validate_wire_array(
1552    schema: &Value,
1553    value: &Value,
1554    path: &str,
1555    source_path: &Path,
1556) -> Result<(), CodegenError> {
1557    let Value::Array(values) = value else {
1558        return invalid_wire_value(path, "expected an array", source_path);
1559    };
1560    if let Some(items) = schema.get("items") {
1561        for (index, item) in values.iter().enumerate() {
1562            validate_wire_value_inner(items, item, &format!("{path}[{index}]"), source_path)?;
1563        }
1564    }
1565    let length = u64::try_from(values.len()).unwrap_or(u64::MAX);
1566    if let Some(minimum) = schema.get("minItems").and_then(Value::as_u64)
1567        && length < minimum
1568    {
1569        return invalid_wire_value(path, "array has fewer items than minItems", source_path);
1570    }
1571    if let Some(maximum) = schema.get("maxItems").and_then(Value::as_u64)
1572        && length > maximum
1573    {
1574        return invalid_wire_value(path, "array has more items than maxItems", source_path);
1575    }
1576    Ok(())
1577}
1578
1579fn validate_wire_string(
1580    schema: &Value,
1581    value: &Value,
1582    path: &str,
1583    source_path: &Path,
1584) -> Result<(), CodegenError> {
1585    let Value::String(value) = value else {
1586        return invalid_wire_value(path, "expected a string", source_path);
1587    };
1588    validate_string_format(schema, value, path, source_path)?;
1589    let length = u64::try_from(value.chars().count()).unwrap_or(u64::MAX);
1590    if let Some(minimum) = schema.get("minLength").and_then(Value::as_u64)
1591        && length < minimum
1592    {
1593        return invalid_wire_value(path, "string is shorter than minLength", source_path);
1594    }
1595    if let Some(maximum) = schema.get("maxLength").and_then(Value::as_u64)
1596        && length > maximum
1597    {
1598        return invalid_wire_value(path, "string is longer than maxLength", source_path);
1599    }
1600    Ok(())
1601}
1602
1603fn validate_numeric_constraint(
1604    schema: &Value,
1605    value: &Value,
1606    path: &str,
1607    source_path: &Path,
1608) -> Result<(), CodegenError> {
1609    let Some(number) = value.as_f64() else {
1610        return Ok(());
1611    };
1612    if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64)
1613        && number < minimum
1614    {
1615        return invalid_wire_value(path, "number is below minimum", source_path);
1616    }
1617    if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64)
1618        && number > maximum
1619    {
1620        return invalid_wire_value(path, "number is above maximum", source_path);
1621    }
1622    if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64)
1623        && number <= minimum
1624    {
1625        return invalid_wire_value(path, "number is not above exclusiveMinimum", source_path);
1626    }
1627    if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64)
1628        && number >= maximum
1629    {
1630        return invalid_wire_value(path, "number is not below exclusiveMaximum", source_path);
1631    }
1632    Ok(())
1633}
1634
1635fn validate_string_format(
1636    schema: &Value,
1637    value: &str,
1638    path: &str,
1639    source_path: &Path,
1640) -> Result<(), CodegenError> {
1641    let Some(format) = schema.get("format").and_then(Value::as_str) else {
1642        return Ok(());
1643    };
1644    let valid = match format {
1645        "int64" => is_signed_decimal(value),
1646        "uint64" => is_unsigned_decimal(value),
1647        "byte" => is_base64(value),
1648        "date-time" => is_rfc3339(value),
1649        "duration" => is_iso8601_duration(value),
1650        _ => false,
1651    };
1652    if valid {
1653        Ok(())
1654    } else {
1655        invalid_wire_value(
1656            path,
1657            &format!("string does not match portable `{format}` format"),
1658            source_path,
1659        )
1660    }
1661}
1662
1663fn invalid_wire_value<T>(path: &str, detail: &str, _source_path: &Path) -> Result<T, CodegenError> {
1664    Err(CodegenError::InvalidPortableValue {
1665        path: path.to_owned(),
1666        detail: detail.to_owned(),
1667    })
1668}
1669
1670fn is_signed_decimal(value: &str) -> bool {
1671    let digits = value.strip_prefix('-').unwrap_or(value);
1672    !digits.is_empty()
1673        && (digits == "0" || !digits.starts_with('0'))
1674        && digits.chars().all(|character| character.is_ascii_digit())
1675        && value.parse::<i64>().is_ok()
1676}
1677
1678fn is_unsigned_decimal(value: &str) -> bool {
1679    !value.is_empty()
1680        && (value == "0" || !value.starts_with('0'))
1681        && value.chars().all(|character| character.is_ascii_digit())
1682        && value.parse::<u64>().is_ok()
1683}
1684
1685fn is_base64(value: &str) -> bool {
1686    let bytes = value.as_bytes();
1687    if bytes.is_empty() {
1688        return true;
1689    }
1690    if !bytes.len().is_multiple_of(4) {
1691        return false;
1692    }
1693    let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count();
1694    if padding > 2 || bytes[..bytes.len() - padding].contains(&b'=') {
1695        return false;
1696    }
1697    for byte in &bytes[..bytes.len() - padding] {
1698        if base64_digit(*byte).is_none() {
1699            return false;
1700        }
1701    }
1702    let last = &bytes[bytes.len() - 4..];
1703    if padding == 1 {
1704        base64_digit(last[2]).is_some_and(|digit| digit.trailing_zeros() >= 2)
1705    } else if padding == 2 {
1706        base64_digit(last[1]).is_some_and(|digit| digit.trailing_zeros() >= 4)
1707    } else {
1708        base64_digit(last[2]).is_some() && base64_digit(last[3]).is_some()
1709    }
1710}
1711
1712fn base64_digit(byte: u8) -> Option<u8> {
1713    match byte {
1714        b'A'..=b'Z' => Some(byte - b'A'),
1715        b'a'..=b'z' => Some(byte - b'a' + 26),
1716        b'0'..=b'9' => Some(byte - b'0' + 52),
1717        b'+' => Some(62),
1718        b'/' => Some(63),
1719        _ => None,
1720    }
1721}
1722
1723fn is_rfc3339(value: &str) -> bool {
1724    let bytes = value.as_bytes();
1725    if bytes.len() < 20
1726        || !matches!(bytes.get(4), Some(b'-'))
1727        || !matches!(bytes.get(7), Some(b'-'))
1728        || !matches!(bytes.get(10), Some(b'T' | b't'))
1729        || !matches!(bytes.get(13), Some(b':'))
1730        || !matches!(bytes.get(16), Some(b':'))
1731    {
1732        return false;
1733    }
1734    let Some(month) = fixed_digits(bytes, 5, 2) else {
1735        return false;
1736    };
1737    let Some(day) = fixed_digits(bytes, 8, 2) else {
1738        return false;
1739    };
1740    let Some(hour) = fixed_digits(bytes, 11, 2) else {
1741        return false;
1742    };
1743    let Some(minute) = fixed_digits(bytes, 14, 2) else {
1744        return false;
1745    };
1746    let Some(second) = fixed_digits(bytes, 17, 2) else {
1747        return false;
1748    };
1749    let Some(year) = fixed_digits(bytes, 0, 4) else {
1750        return false;
1751    };
1752    let max_day = match month {
1753        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1754        4 | 6 | 9 | 11 => 30,
1755        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
1756        2 => 28,
1757        _ => return false,
1758    };
1759    if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
1760        return false;
1761    }
1762    let mut index = 19;
1763    if bytes.get(index) == Some(&b'.') {
1764        index += 1;
1765        let start = index;
1766        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
1767            index += 1;
1768        }
1769        if index == start {
1770            return false;
1771        }
1772    }
1773    match bytes.get(index) {
1774        Some(b'Z' | b'z') => index + 1 == bytes.len(),
1775        Some(b'+' | b'-') => {
1776            index += 1;
1777            let Some(offset_hour) = fixed_digits(bytes, index, 2) else {
1778                return false;
1779            };
1780            index += 2;
1781            if bytes.get(index) != Some(&b':') {
1782                return false;
1783            }
1784            index += 1;
1785            let Some(offset_minute) = fixed_digits(bytes, index, 2) else {
1786                return false;
1787            };
1788            offset_hour <= 23 && offset_minute <= 59 && index + 2 == bytes.len()
1789        }
1790        _ => false,
1791    }
1792}
1793
1794fn fixed_digits(bytes: &[u8], start: usize, length: usize) -> Option<u32> {
1795    let slice = bytes.get(start..start + length)?;
1796    slice.iter().try_fold(0_u32, |value, digit| {
1797        digit
1798            .is_ascii_digit()
1799            .then(|| value * 10 + u32::from(digit - b'0'))
1800    })
1801}
1802
1803fn is_iso8601_duration(value: &str) -> bool {
1804    let bytes = value.as_bytes();
1805    let mut index = usize::from(bytes.first() == Some(&b'-'));
1806    if bytes.get(index) != Some(&b'P') {
1807        return false;
1808    }
1809    index += 1;
1810    let mut in_time = false;
1811    let mut saw_component = false;
1812    let mut saw_time = false;
1813    while index < bytes.len() {
1814        if bytes[index] == b'T' {
1815            if in_time || index + 1 == bytes.len() {
1816                return false;
1817            }
1818            in_time = true;
1819            index += 1;
1820            continue;
1821        }
1822        let start = index;
1823        let mut separator_seen = false;
1824        while index < bytes.len()
1825            && (bytes[index].is_ascii_digit() || (!separator_seen && bytes[index] == b'.'))
1826        {
1827            separator_seen |= bytes[index] == b'.';
1828            index += 1;
1829        }
1830        if index == start || bytes.get(index).is_none() {
1831            return false;
1832        }
1833        let unit = bytes[index];
1834        let valid_unit = if in_time {
1835            matches!(unit, b'H' | b'M' | b'S')
1836        } else {
1837            matches!(unit, b'Y' | b'M' | b'W' | b'D')
1838        };
1839        if !valid_unit {
1840            return false;
1841        }
1842        if in_time {
1843            saw_time = true;
1844        }
1845        saw_component = true;
1846        index += 1;
1847    }
1848    saw_component && (!in_time || saw_time)
1849}
1850
1851fn validate_portable_value(value: &Value, path: String) -> Result<(), CodegenError> {
1852    match value {
1853        Value::Number(number) => {
1854            if !is_safe_json_number(number) {
1855                return Err(CodegenError::InvalidPortableValue {
1856                    path,
1857                    detail: "wide integers must use an explicit decimal-string format".to_owned(),
1858                });
1859            }
1860        }
1861        Value::Array(values) => {
1862            for (index, value) in values.iter().enumerate() {
1863                validate_portable_value(value, format!("{path}[{index}]"))?;
1864            }
1865        }
1866        Value::Object(values) => {
1867            for (key, value) in values {
1868                validate_portable_value(value, format!("{path}.{key}"))?;
1869            }
1870        }
1871        Value::Null | Value::Bool(_) | Value::String(_) => {}
1872    }
1873    Ok(())
1874}
1875
1876fn write_artifact(path: &Path, contents: &str) -> Result<(), CodegenError> {
1877    if let Some(parent) = path.parent() {
1878        fs::create_dir_all(parent).map_err(|source| CodegenError::Io {
1879            path: parent.to_path_buf(),
1880            source,
1881        })?;
1882    }
1883    fs::write(path, contents).map_err(|source| CodegenError::Io {
1884        path: path.to_path_buf(),
1885        source,
1886    })
1887}
1888
1889fn check_artifact(path: &Path, expected: &str) -> Result<(), CodegenError> {
1890    let actual = fs::read_to_string(path).map_err(|source| CodegenError::Io {
1891        path: path.to_path_buf(),
1892        source,
1893    })?;
1894    if actual == expected {
1895        Ok(())
1896    } else {
1897        Err(CodegenError::GeneratedArtifactDrift {
1898            path: path.to_path_buf(),
1899        })
1900    }
1901}
1902
1903/// Checks whether `new_path` is an additive, compatible evolution of
1904/// `old_path`. `true` means the same Capability series remains compatible.
1905pub fn lint_compatibility(old_path: &Path, new_path: &Path) -> Result<bool, CompatibilityError> {
1906    let old = load_descriptor(old_path).map_err(|error| CompatibilityError::BreakingChanges {
1907        changes: vec![error.to_string()],
1908    })?;
1909    let new = load_descriptor(new_path).map_err(|error| CompatibilityError::BreakingChanges {
1910        changes: vec![error.to_string()],
1911    })?;
1912    if old.capability_id != new.capability_id {
1913        return Err(CompatibilityError::IdentityChanged {
1914            from: old.capability_id,
1915            to: new.capability_id,
1916        });
1917    }
1918    if new.parsed_version <= old.parsed_version {
1919        return Err(CompatibilityError::VersionNotAdvanced {
1920            from: old.version,
1921            to: new.version,
1922        });
1923    }
1924    if new.parsed_version.major() != old.parsed_version.major() {
1925        return Err(CompatibilityError::BreakingChanges {
1926            changes: vec!["a breaking change must use a new Capability @major identity".to_owned()],
1927        });
1928    }
1929
1930    let mut changes = Vec::new();
1931    if old.portable != new.portable {
1932        changes.push("Descriptor portability changed".to_owned());
1933    }
1934    if old.cross_lane_transfer && !new.cross_lane_transfer {
1935        changes.push("Descriptor cross-lane transfer support was removed".to_owned());
1936    }
1937    let old_operations: BTreeMap<_, _> = old
1938        .operations
1939        .iter()
1940        .map(|operation| (operation.name.as_str(), operation))
1941        .collect();
1942    let new_operations: BTreeMap<_, _> = new
1943        .operations
1944        .iter()
1945        .map(|operation| (operation.name.as_str(), operation))
1946        .collect();
1947
1948    for (name, old_operation) in &old_operations {
1949        let Some(new_operation) = new_operations.get(name) else {
1950            changes.push(format!("Operation `{name}` was removed"));
1951            continue;
1952        };
1953        if old_operation.interaction != new_operation.interaction {
1954            changes.push(format!("Operation `{name}` changed interaction"));
1955        }
1956        compare_schema(
1957            &old_operation.request_schema,
1958            &new_operation.request_schema,
1959            &format!("Operation `{name}` request"),
1960            &mut changes,
1961        );
1962        compare_schema(
1963            &old_operation.response_schema,
1964            &new_operation.response_schema,
1965            &format!("Operation `{name}` response"),
1966            &mut changes,
1967        );
1968        compare_error_schema(
1969            &old_operation.domain_error_schema,
1970            &new_operation.domain_error_schema,
1971            &format!("Operation `{name}` Domain Error"),
1972            &mut changes,
1973        );
1974    }
1975
1976    if new.parsed_version.minor() == old.parsed_version.minor() {
1977        if contract_signature(&old) != contract_signature(&new) {
1978            changes.push("a patch release changed the observable contract".to_owned());
1979        }
1980        for name in new_operations
1981            .keys()
1982            .filter(|name| !old_operations.contains_key(**name))
1983        {
1984            changes.push(format!("Operation `{name}` was added in a patch release"));
1985        }
1986    }
1987    if new.parsed_version.minor() < old.parsed_version.minor() {
1988        changes.push("Descriptor minor version moved backwards".to_owned());
1989    }
1990
1991    if changes.is_empty() {
1992        Ok(true)
1993    } else {
1994        Err(CompatibilityError::BreakingChanges { changes })
1995    }
1996}
1997
1998fn contract_signature(descriptor: &Descriptor) -> String {
1999    let operations = descriptor
2000        .operations
2001        .iter()
2002        .map(|operation| {
2003            serde_json::json!({
2004                "name": operation.name,
2005                "interaction": operation.interaction,
2006                "request": operation.request_schema,
2007                "response": operation.response_schema,
2008                "domain_error": canonical_domain_error_schema(&operation.domain_error_schema),
2009            })
2010        })
2011        .collect::<Vec<_>>();
2012    canonical_json(&Value::Array(operations))
2013}
2014
2015fn canonical_domain_error_schema(schema: &Value) -> Value {
2016    let Some(variants) = schema.get("oneOf").and_then(Value::as_array) else {
2017        return schema.clone();
2018    };
2019    let mut variants = variants.clone();
2020    variants.sort_by(|left, right| {
2021        error_variant_code(left)
2022            .unwrap_or_default()
2023            .cmp(error_variant_code(right).unwrap_or_default())
2024    });
2025    let mut schema = schema.as_object().cloned().unwrap_or_default();
2026    schema.insert("oneOf".to_owned(), Value::Array(variants));
2027    Value::Object(schema)
2028}
2029
2030fn error_variant_code(variant: &Value) -> Option<&str> {
2031    variant.get("const").and_then(Value::as_str).or_else(|| {
2032        variant
2033            .get("properties")
2034            .and_then(Value::as_object)
2035            .and_then(|properties| properties.get("code"))
2036            .and_then(|code| code.get("const"))
2037            .and_then(Value::as_str)
2038    })
2039}
2040
2041fn compare_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2042    let old_nullable = is_nullable(old);
2043    let new_nullable = is_nullable(new);
2044    if old_nullable != new_nullable {
2045        changes.push(format!("{location} changed nullability"));
2046        return;
2047    }
2048    let old_type = schema_type_name(old);
2049    let new_type = schema_type_name(new);
2050    if old_type != new_type {
2051        changes.push(format!("{location} changed type"));
2052        return;
2053    }
2054    match old_type {
2055        Some("object") => {
2056            let old_properties = old.get("properties").and_then(Value::as_object);
2057            let new_properties = new.get("properties").and_then(Value::as_object);
2058            let (Some(old_properties), Some(new_properties)) = (old_properties, new_properties)
2059            else {
2060                if canonical_json(old) != canonical_json(new) {
2061                    changes.push(format!("{location} changed object shape"));
2062                }
2063                return;
2064            };
2065            let old_required = required_fields(old);
2066            let new_required = required_fields(new);
2067            for name in old_properties.keys() {
2068                let Some(new_schema) = new_properties.get(name) else {
2069                    changes.push(format!("{location} field `{name}` was removed"));
2070                    continue;
2071                };
2072                compare_schema(
2073                    &old_properties[name],
2074                    new_schema,
2075                    &format!("{location} field `{name}`"),
2076                    changes,
2077                );
2078            }
2079            for name in new_required.difference(&old_required) {
2080                changes.push(format!("{location} field `{name}` became required"));
2081            }
2082            for name in old_required.difference(&new_required) {
2083                changes.push(format!("{location} field `{name}` became optional"));
2084            }
2085            if old.get("additionalProperties") != new.get("additionalProperties") {
2086                changes.push(format!("{location} changed additional-properties policy"));
2087            }
2088            if schema_constraints_signature(old) != schema_constraints_signature(new) {
2089                changes.push(format!("{location} changed constraints"));
2090            }
2091        }
2092        Some("array") => {
2093            if let (Some(old_items), Some(new_items)) = (old.get("items"), new.get("items")) {
2094                compare_schema(old_items, new_items, &format!("{location} items"), changes);
2095            }
2096            if schema_constraints_signature(old) != schema_constraints_signature(new) {
2097                changes.push(format!("{location} changed constraints"));
2098            }
2099        }
2100        _ => {
2101            if canonical_json(old) != canonical_json(new) {
2102                changes.push(format!("{location} changed format or constraints"));
2103            }
2104        }
2105    }
2106}
2107
2108fn compare_error_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2109    let old_variants = error_variant_values(old);
2110    let new_variants = error_variant_values(new);
2111    if old_variants.is_empty() || new_variants.is_empty() {
2112        if canonical_json(old) != canonical_json(new) {
2113            changes.push(format!("{location} changed shape"));
2114        }
2115        return;
2116    }
2117    for (code, schema) in old_variants {
2118        match new_variants.get(&code) {
2119            None => changes.push(format!("{location} variant `{code}` was removed")),
2120            Some(new_schema) => compare_schema(
2121                &schema,
2122                new_schema,
2123                &format!("{location} variant `{code}`"),
2124                changes,
2125            ),
2126        }
2127    }
2128}
2129
2130fn error_variant_ir_definitions(schema: &Value) -> Vec<ErrorVariantIr> {
2131    let mut variants = schema
2132        .get("oneOf")
2133        .and_then(Value::as_array)
2134        .into_iter()
2135        .flatten()
2136        .filter_map(|variant| {
2137            if let Some(code) = variant.get("const").and_then(Value::as_str) {
2138                return Some(ErrorVariantIr {
2139                    code: code.to_owned(),
2140                    name: pascal_case(code),
2141                    structured: false,
2142                    payload: None,
2143                    payload_required: false,
2144                });
2145            }
2146            let object = variant.as_object()?;
2147            let code = object
2148                .get("properties")
2149                .and_then(Value::as_object)
2150                .and_then(|properties| properties.get("code"))
2151                .and_then(|code| code.get("const"))
2152                .and_then(Value::as_str)?;
2153            Some(ErrorVariantIr {
2154                code: code.to_owned(),
2155                name: pascal_case(code),
2156                structured: true,
2157                payload: object
2158                    .get("properties")
2159                    .and_then(Value::as_object)
2160                    .and_then(|properties| properties.get("payload"))
2161                    .map(type_ir_from_schema),
2162                payload_required: required_fields(variant).contains("payload"),
2163            })
2164        })
2165        .collect::<Vec<_>>();
2166    variants.sort_by(|left, right| left.code.cmp(&right.code));
2167    variants
2168}
2169
2170fn error_variant_values(schema: &Value) -> BTreeMap<String, Value> {
2171    let variants = schema
2172        .get("oneOf")
2173        .and_then(Value::as_array)
2174        .into_iter()
2175        .flatten();
2176    variants
2177        .filter_map(|variant| {
2178            let code = if let Some(code) = variant.get("const").and_then(Value::as_str) {
2179                code.to_owned()
2180            } else {
2181                variant
2182                    .get("properties")
2183                    .and_then(Value::as_object)
2184                    .and_then(|properties| properties.get("code"))
2185                    .and_then(|code| code.get("const"))
2186                    .and_then(Value::as_str)
2187                    .map(ToOwned::to_owned)?
2188            };
2189            Some((code, variant.clone()))
2190        })
2191        .collect()
2192}
2193
2194fn required_fields(schema: &Value) -> BTreeSet<String> {
2195    schema
2196        .get("required")
2197        .and_then(Value::as_array)
2198        .into_iter()
2199        .flatten()
2200        .filter_map(Value::as_str)
2201        .map(ToOwned::to_owned)
2202        .collect()
2203}
2204
2205fn is_nullable(schema: &Value) -> bool {
2206    schema
2207        .get("type")
2208        .and_then(Value::as_array)
2209        .is_some_and(|types| types.iter().any(|value| value == "null"))
2210        || schema
2211            .get("anyOf")
2212            .and_then(Value::as_array)
2213            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
2214        || schema
2215            .get("oneOf")
2216            .and_then(Value::as_array)
2217            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
2218}
2219
2220fn is_null_schema(schema: &Value) -> bool {
2221    schema.get("type").is_some_and(|value| value == "null")
2222        || schema.get("const").is_some_and(Value::is_null)
2223}
2224
2225fn schema_type_name(schema: &Value) -> Option<&str> {
2226    schema.get("type").and_then(|value| match value {
2227        Value::String(value) => Some(value.as_str()),
2228        Value::Array(values) => values
2229            .iter()
2230            .find_map(Value::as_str)
2231            .filter(|value| *value != "null"),
2232        _ => None,
2233    })
2234}
2235
2236fn canonical_json(value: &Value) -> String {
2237    serde_json::to_string(value).expect("JSON values are serializable")
2238}
2239
2240fn schema_constraints_signature(schema: &Value) -> String {
2241    let mut schema = schema.as_object().cloned().unwrap_or_default();
2242    for key in [
2243        "type",
2244        "properties",
2245        "required",
2246        "additionalProperties",
2247        "items",
2248    ] {
2249        schema.remove(key);
2250    }
2251    canonical_json(&Value::Object(schema))
2252}
2253
2254fn is_identifier(value: &str) -> bool {
2255    let mut characters = value.chars();
2256    characters
2257        .next()
2258        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2259        && characters
2260            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
2261}
2262
2263fn is_rust_member_name(value: &str) -> bool {
2264    !matches!(snake_case(value).as_str(), "self" | "super" | "crate")
2265}
2266
2267fn is_rust_type_identifier(value: &str) -> bool {
2268    let mut characters = value.chars();
2269    let valid = characters
2270        .next()
2271        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2272        && characters.all(|character| character.is_ascii_alphanumeric() || character == '_');
2273    valid && value != "Self"
2274}
2275
2276fn is_generated_type_name(value: &str) -> bool {
2277    is_rust_type_identifier(value)
2278        && !matches!(
2279            value,
2280            "Self"
2281                | "String"
2282                | "Int64"
2283                | "Uint64"
2284                | "Bytes"
2285                | "Timestamp"
2286                | "Duration"
2287                | "OptionalValue"
2288                | "UnknownDomainError"
2289                | "InvocationContext"
2290                | "ModuleDependencies"
2291                | "NativeRequestEndpoint"
2292                | "NativeRequestHandle"
2293                | "RequestCapability"
2294                | "RuntimeFailure"
2295                | "LocalBoxFuture"
2296        )
2297}
2298
2299fn is_generated_enum_variant(value: &str) -> bool {
2300    is_rust_type_identifier(value)
2301}
2302
2303fn pascal_case(value: &str) -> String {
2304    let mut output = String::new();
2305    for part in value.split(|character: char| !character.is_ascii_alphanumeric()) {
2306        if part.is_empty() || part.chars().all(char::is_numeric) {
2307            continue;
2308        }
2309        let mut chars = part.chars();
2310        if let Some(first) = chars.next() {
2311            output.extend(first.to_uppercase());
2312            output.push_str(chars.as_str());
2313        }
2314    }
2315    if output.is_empty() {
2316        "Value".to_owned()
2317    } else if output
2318        .chars()
2319        .next()
2320        .is_some_and(|character| character.is_ascii_digit())
2321    {
2322        format!("Value{output}")
2323    } else {
2324        output
2325    }
2326}
2327
2328fn snake_case(value: &str) -> String {
2329    let mut output = String::new();
2330    for (index, character) in value.chars().enumerate() {
2331        if character.is_ascii_alphanumeric() {
2332            if character.is_ascii_uppercase() && index != 0 {
2333                output.push('_');
2334            }
2335            output.push(character.to_ascii_lowercase());
2336        } else if !output.ends_with('_') {
2337            output.push('_');
2338        }
2339    }
2340    let output = output.trim_matches('_').to_owned();
2341    if output.is_empty() {
2342        "value".to_owned()
2343    } else {
2344        output
2345    }
2346}
2347
2348fn screaming_snake_case(value: &str) -> String {
2349    snake_case(value).to_ascii_uppercase()
2350}
2351
2352fn rust_field_name(value: &str) -> String {
2353    let name = snake_case(value);
2354    if matches!(name.as_str(), "self" | "super" | "crate") {
2355        return format!("{name}_");
2356    }
2357    if matches!(
2358        name.as_str(),
2359        "as" | "break"
2360            | "const"
2361            | "continue"
2362            | "crate"
2363            | "else"
2364            | "enum"
2365            | "extern"
2366            | "false"
2367            | "fn"
2368            | "for"
2369            | "if"
2370            | "impl"
2371            | "in"
2372            | "let"
2373            | "loop"
2374            | "match"
2375            | "mod"
2376            | "move"
2377            | "mut"
2378            | "pub"
2379            | "ref"
2380            | "return"
2381            | "self"
2382            | "Self"
2383            | "static"
2384            | "struct"
2385            | "super"
2386            | "trait"
2387            | "true"
2388            | "type"
2389            | "unsafe"
2390            | "use"
2391            | "where"
2392            | "while"
2393            | "async"
2394            | "await"
2395            | "dyn"
2396    ) {
2397        format!("r#{name}")
2398    } else {
2399        name
2400    }
2401}
2402
2403fn quote_string(value: &str) -> String {
2404    serde_json::to_string(value).expect("string is serializable")
2405}
2406
2407fn typescript_property_name(value: &str) -> String {
2408    let mut characters = value.chars();
2409    let valid_identifier = characters
2410        .next()
2411        .is_some_and(|character| character.is_ascii_alphabetic() || matches!(character, '_' | '$'))
2412        && characters
2413            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$'));
2414    if valid_identifier {
2415        value.to_owned()
2416    } else {
2417        quote_string(value)
2418    }
2419}
2420
2421struct RustTypes {
2422    declarations: Vec<String>,
2423    declared: BTreeSet<String>,
2424}
2425
2426impl RustTypes {
2427    fn new() -> Self {
2428        Self {
2429            declarations: Vec::new(),
2430            declared: BTreeSet::new(),
2431        }
2432    }
2433
2434    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
2435        if !self.declared.insert(name.to_owned()) {
2436            return name.to_owned();
2437        }
2438        let placeholder = self.declarations.len();
2439        self.declarations.push(String::new());
2440        let mut rendered_fields = Vec::new();
2441        for field in fields {
2442            let type_name = self.type_for_non_null(
2443                field.ty.non_null(),
2444                &format!("{name}{}", pascal_case(&field.name)),
2445            );
2446            let field_type = if field.required {
2447                if field.ty.is_nullable() {
2448                    format!("Option<{type_name}>")
2449                } else {
2450                    type_name
2451                }
2452            } else if field.ty.is_nullable() {
2453                format!("OptionalValue<{type_name}>")
2454            } else {
2455                format!("Option<{type_name}>")
2456            };
2457            let mut attributes = vec![format!(
2458                "    #[serde(rename = {})]",
2459                quote_string(&field.name)
2460            )];
2461            if !field.required {
2462                if field.ty.is_nullable() {
2463                    attributes.push("    #[serde(default)]".to_owned());
2464                    attributes
2465                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
2466                    attributes.push(
2467                        "    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_optional_value\")]"
2468                            .to_owned(),
2469                    );
2470                } else {
2471                    attributes
2472                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
2473                }
2474            }
2475            if field.required {
2476                attributes
2477                    .push("    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_required\")]".to_owned());
2478            }
2479            rendered_fields.push(format!(
2480                "{}\n    pub {}: {field_type},",
2481                attributes.join("\n"),
2482                rust_field_name(&field.name)
2483            ));
2484        }
2485        let debug_impl = rust_debug::render(
2486            name,
2487            fields.iter().map(|field| {
2488                (
2489                    field.name.as_str(),
2490                    rust_field_name(&field.name),
2491                    field.sensitive,
2492                )
2493            }),
2494        );
2495        let derives = if debug_impl.is_some() {
2496            "Clone, PartialEq, serde::Serialize, serde::Deserialize"
2497        } else {
2498            "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
2499        };
2500        self.declarations[placeholder] = format!(
2501            "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
2502            rendered_fields.join("\n"),
2503            debug_impl = debug_impl.unwrap_or_default(),
2504        );
2505        name.to_owned()
2506    }
2507
2508    fn enum_type(&mut self, name: &str, values: &[String]) -> String {
2509        if !self.declared.insert(name.to_owned()) {
2510            return name.to_owned();
2511        }
2512        let variants = values
2513            .iter()
2514            .map(|value| {
2515                format!(
2516                    "    #[serde(rename = {})]\n    {},",
2517                    quote_string(value),
2518                    pascal_case(value)
2519                )
2520            })
2521            .collect::<Vec<_>>();
2522        self.declarations.push(format!(
2523            "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
2524            variants.join("\n")
2525        ));
2526        name.to_owned()
2527    }
2528
2529    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2530        let base = self.type_for_non_null(ty.non_null(), nested_name);
2531        if ty.is_nullable() {
2532            format!("Option<{base}>")
2533        } else {
2534            base
2535        }
2536    }
2537
2538    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2539        match ty {
2540            TypeIr::Any => "serde_json::Value".to_owned(),
2541            TypeIr::String => "String".to_owned(),
2542            TypeIr::Enum(values) => self.enum_type(nested_name, values),
2543            TypeIr::Int64 => "Int64".to_owned(),
2544            TypeIr::Uint64 => "Uint64".to_owned(),
2545            TypeIr::Bytes => "Bytes".to_owned(),
2546            TypeIr::Timestamp => "Timestamp".to_owned(),
2547            TypeIr::Duration => "Duration".to_owned(),
2548            TypeIr::Integer => "i64".to_owned(),
2549            TypeIr::Number => "f64".to_owned(),
2550            TypeIr::Boolean => "bool".to_owned(),
2551            TypeIr::Null => "()".to_owned(),
2552            TypeIr::Array(items) => {
2553                format!(
2554                    "Vec<{}>",
2555                    self.type_for(items, &format!("{nested_name}Item"))
2556                )
2557            }
2558            TypeIr::Object { fields, additional } => {
2559                if fields.is_empty() {
2560                    match additional {
2561                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
2562                        ObjectAdditionalIr::Any => {
2563                            "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
2564                        }
2565                        ObjectAdditionalIr::Typed(values) => format!(
2566                            "std::collections::BTreeMap<String, {}>",
2567                            self.type_for(values, &format!("{nested_name}Value"))
2568                        ),
2569                    }
2570                } else {
2571                    self.object(nested_name, fields)
2572                }
2573            }
2574            TypeIr::Nullable(inner) => {
2575                format!("Option<{}>", self.type_for_non_null(inner, nested_name))
2576            }
2577        }
2578    }
2579}
2580
2581struct TypeScriptTypes {
2582    declarations: Vec<String>,
2583    declared: BTreeSet<String>,
2584}
2585
2586impl TypeScriptTypes {
2587    fn new() -> Self {
2588        Self {
2589            declarations: Vec::new(),
2590            declared: BTreeSet::new(),
2591        }
2592    }
2593
2594    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
2595        if !self.declared.insert(name.to_owned()) {
2596            return name.to_owned();
2597        }
2598        let placeholder = self.declarations.len();
2599        self.declarations.push(String::new());
2600        let mut rendered_fields = Vec::new();
2601        for field in fields {
2602            let type_name = self.type_for_non_null(
2603                field.ty.non_null(),
2604                &format!("{name}{}", pascal_case(&field.name)),
2605            );
2606            let optional = if field.required { "" } else { "?" };
2607            let field_type = if field.ty.is_nullable() {
2608                format!("{type_name} | null")
2609            } else {
2610                type_name
2611            };
2612            rendered_fields.push(format!(
2613                "  {}{optional}: {field_type};",
2614                typescript_property_name(&field.name)
2615            ));
2616        }
2617        self.declarations[placeholder] = format!(
2618            "export interface {name} {{\n{}\n}}\n",
2619            rendered_fields.join("\n")
2620        );
2621        name.to_owned()
2622    }
2623
2624    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2625        let base = self.type_for_non_null(ty.non_null(), nested_name);
2626        if ty.is_nullable() {
2627            format!("{base} | null")
2628        } else {
2629            base
2630        }
2631    }
2632
2633    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2634        match ty {
2635            TypeIr::Any => "unknown".to_owned(),
2636            TypeIr::String => "string".to_owned(),
2637            TypeIr::Int64 => "Int64".to_owned(),
2638            TypeIr::Uint64 => "Uint64".to_owned(),
2639            TypeIr::Bytes => "Bytes".to_owned(),
2640            TypeIr::Timestamp => "Timestamp".to_owned(),
2641            TypeIr::Duration => "Duration".to_owned(),
2642            TypeIr::Integer | TypeIr::Number => "number".to_owned(),
2643            TypeIr::Boolean => "boolean".to_owned(),
2644            TypeIr::Null => "null".to_owned(),
2645            TypeIr::Enum(values) => values
2646                .iter()
2647                .map(|value| quote_string(value))
2648                .collect::<Vec<_>>()
2649                .join(" | "),
2650            TypeIr::Array(items) => {
2651                format!(
2652                    "Array<{}>",
2653                    self.type_for(items, &format!("{nested_name}Item"))
2654                )
2655            }
2656            TypeIr::Object { fields, additional } => {
2657                if fields.is_empty() {
2658                    match additional {
2659                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
2660                        ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
2661                        ObjectAdditionalIr::Typed(values) => format!(
2662                            "Record<string, {}>",
2663                            self.type_for(values, &format!("{nested_name}Value"))
2664                        ),
2665                    }
2666                } else {
2667                    self.object(nested_name, fields)
2668                }
2669            }
2670            TypeIr::Nullable(inner) => {
2671                format!("{} | null", self.type_for_non_null(inner, nested_name))
2672            }
2673        }
2674    }
2675}
2676
2677#[allow(clippy::too_many_lines)]
2678fn generate_rust(contract: &ContractIr) -> String {
2679    let capability_name = pascal_case(
2680        contract
2681            .capability_id
2682            .split('@')
2683            .next()
2684            .and_then(|identity| identity.rsplit('.').next())
2685            .unwrap_or("Capability"),
2686    );
2687    let capability_const = screaming_snake_case(&capability_name);
2688    let native_support_name = format!("__LensoNativeSupport{capability_name}");
2689    let mut types = RustTypes::new();
2690    let mut operation_rows = Vec::new();
2691    let mut stream_operation_rows = Vec::new();
2692    let mut event_operation_rows = Vec::new();
2693    let mut operation_markers = Vec::new();
2694    let mut provider_methods = Vec::new();
2695    let mut provider_lowering_methods = Vec::new();
2696    let mut provider_result_conversions = Vec::new();
2697    let mut endpoint_arms = Vec::new();
2698    let mut stream_endpoint_arms = Vec::new();
2699    let mut event_endpoint_arms = Vec::new();
2700    let mut client_fields = Vec::new();
2701    let mut client_initializers = Vec::new();
2702    let mut client_methods = Vec::new();
2703    let mut invocation_errors = Vec::new();
2704    let mut error_codecs = Vec::new();
2705    let mut wire_codecs = Vec::new();
2706
2707    for operation in &contract.operations {
2708        let operation_name = pascal_case(&operation.name);
2709        let request_name = format!("{operation_name}Request");
2710        let response_name = format!("{operation_name}Response");
2711        let error_name = format!("{operation_name}Error");
2712        let marker_name = if contract.operations.len() == 1 {
2713            capability_name.clone()
2714        } else {
2715            format!("{capability_name}{operation_name}")
2716        };
2717        let request_type = types.type_for(&operation.request, &request_name);
2718        let response_type = types.type_for(&operation.response, &response_name);
2719        let known_errors = &operation.domain_errors;
2720        let error_definition = if known_errors.is_empty() {
2721            format!(
2722                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n    Unknown(UnknownDomainError),\n}}\n"
2723            )
2724        } else {
2725            let variants = known_errors
2726                .iter()
2727                .map(|variant| {
2728                    if let Some(payload) = &variant.payload {
2729                        let payload_name = format!("{error_name}{}Payload", variant.name);
2730                        let payload_type =
2731                            types.type_for_non_null(payload.non_null(), &payload_name);
2732                        let payload_type = if variant.payload_required {
2733                            if payload.is_nullable() {
2734                                format!("Option<{payload_type}>")
2735                            } else {
2736                                payload_type
2737                            }
2738                        } else if payload.is_nullable() {
2739                            format!("OptionalValue<{payload_type}>")
2740                        } else {
2741                            format!("Option<{payload_type}>")
2742                        };
2743                        format!("    {} {{ payload: {payload_type} }},", variant.name)
2744                    } else {
2745                        format!("    {},", variant.name)
2746                    }
2747                })
2748                .chain(std::iter::once(
2749                    "    Unknown(UnknownDomainError),".to_owned(),
2750                ))
2751                .collect::<Vec<_>>();
2752            format!(
2753                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
2754                variants.join("\n")
2755            )
2756        };
2757        types.declarations.push(error_definition);
2758        error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
2759        wire_codecs.push(generate_rust_wire_codecs(
2760            &operation.name,
2761            &request_type,
2762            &response_type,
2763            &error_name,
2764        ));
2765        if operation.interaction == "event" {
2766            wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
2767        }
2768        let operation_const = screaming_snake_case(&operation.name);
2769        operation_markers.push(match operation.interaction.as_str() {
2770            "request" => {
2771                let provider_method = rust_field_name(&operation.name);
2772                format!(
2773            "#[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"
2774                )
2775            }
2776            "stream" => format!(
2777                "#[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"
2778            ),
2779            "event" => format!(
2780                "#[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"
2781            ),
2782            _ => unreachable!("Descriptor validation restricts interactions"),
2783        });
2784        if operation.interaction == "request" {
2785            let invocation_error_name = if contract.operations.len() == 1 {
2786                format!("{capability_name}InvocationError")
2787            } else {
2788                format!("{capability_name}{operation_name}InvocationError")
2789            };
2790            operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
2791            provider_methods.push(format!(
2792                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
2793                rust_field_name(&operation.name),
2794            ));
2795            let field = rust_field_name(&operation.name);
2796            let conversion = format!("__LensoInto{capability_name}{operation_name}Result");
2797            provider_result_conversions.push(format!(
2798                "#[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<{response_type}, lenso_module_authoring::ModuleError<{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_module_authoring::ModuleError::Domain(error)) => Ok(Err(error)),\n            Err(lenso_module_authoring::ModuleError::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"
2799            ));
2800            provider_lowering_methods.push(format!(
2801                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let module = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$module>::{field}(&module, context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
2802            ));
2803            endpoint_arms.push(format!(
2804                "            {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            }}",
2805                rust_field_name(&operation.name),
2806            ));
2807            let field = rust_field_name(&operation.name);
2808            client_fields.push(format!("    {field}: NativeRequestHandle<{marker_name}>,"));
2809            client_initializers.push(format!(
2810                "            {field}: dependencies.one::<{marker_name}>()?,"
2811            ));
2812            client_methods.push(format!(
2813                "    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    }}",
2814                screaming_snake_case(&operation.name),
2815                screaming_snake_case(&operation.name),
2816            ));
2817            invocation_errors.push(format!(
2818                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
2819            ));
2820        } else if operation.interaction == "stream" {
2821            let invocation_error_name = if contract.operations.len() == 1 {
2822                format!("{capability_name}InvocationError")
2823            } else {
2824                format!("{capability_name}{operation_name}InvocationError")
2825            };
2826            stream_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
2827            provider_methods.push(format!(
2828                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
2829                rust_field_name(&operation.name)
2830            ));
2831            let field = rust_field_name(&operation.name);
2832            let conversion = format!("__LensoInto{capability_name}{operation_name}StreamResult");
2833            provider_result_conversions.push(format!(
2834                "#[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_module_authoring::ModuleError<{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_module_authoring::ModuleError::Domain(error)) => Err({invocation_error_name}::Domain(error)),\n            Err(lenso_module_authoring::ModuleError::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"
2835            ));
2836            provider_lowering_methods.push(format!(
2837                "        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 module = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$module>::{field}(&module, context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
2838            ));
2839            stream_endpoint_arms.push(format!(
2840                "            {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            }}",
2841                rust_field_name(&operation.name),
2842            ));
2843            let field = rust_field_name(&operation.name);
2844            client_fields.push(format!("    {field}: NativeStreamHandle<{marker_name}>,"));
2845            client_initializers.push(format!(
2846                "            {field}: dependencies.one_stream::<{marker_name}>()?,"
2847            ));
2848            client_methods.push(format!(
2849                "    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    }}"
2850            ));
2851            invocation_errors.push(format!(
2852                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
2853            ));
2854        } else {
2855            event_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
2856            provider_methods.push(format!(
2857                "    fn {}(&self, context: InvocationContext, event: {request_type}) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;",
2858                rust_field_name(&operation.name)
2859            ));
2860            let field = rust_field_name(&operation.name);
2861            let conversion = format!("__LensoInto{capability_name}{operation_name}EventResult");
2862            provider_result_conversions.push(format!(
2863                "#[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"
2864            ));
2865            provider_lowering_methods.push(format!(
2866                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let module = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$module>::{field}(&module, context, event).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
2867            ));
2868            event_endpoint_arms.push(format!(
2869                "            {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            }}",
2870                rust_field_name(&operation.name),
2871            ));
2872            let field = rust_field_name(&operation.name);
2873            client_fields.push(format!("    {field}: NativeEventHandle<{marker_name}>,"));
2874            client_initializers.push(format!(
2875                "            {field}: dependencies.many_event::<{marker_name}>()?,"
2876            ));
2877            client_methods.push(format!(
2878                "    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    }}"
2879            ));
2880        }
2881    }
2882
2883    let request_endpoint_impl = if operation_rows.is_empty() {
2884        String::new()
2885    } else {
2886        format!(
2887            "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",
2888            operation_rows.concat(),
2889            endpoint_arms.join(",\n")
2890        )
2891    };
2892    let stream_endpoint_impl = if stream_operation_rows.is_empty() {
2893        String::new()
2894    } else {
2895        format!(
2896            "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",
2897            stream_operation_rows.concat(),
2898            stream_endpoint_arms.join(",\n")
2899        )
2900    };
2901    let event_endpoint_impl = if event_operation_rows.is_empty() {
2902        String::new()
2903    } else {
2904        format!(
2905            "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",
2906            event_operation_rows.concat(),
2907            event_endpoint_arms.join(",\n")
2908        )
2909    };
2910    let mut output = String::new();
2911    output.push_str(GENERATED_HEADER);
2912    let has_request_operations = !operation_rows.is_empty();
2913    let has_stream_operations = !stream_operation_rows.is_empty();
2914    let has_event_operations = !event_operation_rows.is_empty();
2915    let mut kernel_imports = vec!["InvocationContext", "ModuleDependencies", "RuntimeFailure"];
2916    if has_request_operations {
2917        kernel_imports.extend([
2918            "NativeRequestFuture",
2919            "NativeRequestEndpoint",
2920            "NativeRequestHandle",
2921            "RequestCapability",
2922        ]);
2923    }
2924    if has_stream_operations {
2925        kernel_imports.extend([
2926            "NativeStream",
2927            "NativeStreamEndpoint",
2928            "NativeStreamHandle",
2929            "NativeStreamSession",
2930            "StreamCapability",
2931            "StreamEvent",
2932        ]);
2933    }
2934    if has_event_operations {
2935        kernel_imports.extend([
2936            "EventCapability",
2937            "EventPublishResult",
2938            "NativeEventEndpoint",
2939            "NativeEventHandle",
2940        ]);
2941    }
2942    kernel_imports.sort_unstable();
2943    writeln!(
2944        output,
2945        "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
2946        kernel_imports.join(", ")
2947    )
2948    .expect("writing to a String cannot fail");
2949    output.push_str("use lenso_module_authoring::CapabilityClient;\n");
2950    writeln!(
2951        output,
2952        "pub const CAPABILITY_ID: &str = {};",
2953        quote_string(&contract.capability_id)
2954    )
2955    .expect("writing to a String cannot fail");
2956    writeln!(
2957        output,
2958        "pub const DESCRIPTOR_VERSION: &str = {};",
2959        quote_string(&contract.version)
2960    )
2961    .expect("writing to a String cannot fail");
2962    writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
2963        .expect("writing to a String cannot fail");
2964    writeln!(
2965        output,
2966        "pub const CROSS_LANE_TRANSFER: bool = {};",
2967        contract.cross_lane_transfer
2968    )
2969    .expect("writing to a String cannot fail");
2970    writeln!(
2971        output,
2972        "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
2973    )
2974    .expect("writing to a String cannot fail");
2975    write!(
2976        output,
2977        "pub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\n\n"
2978    )
2979    .expect("writing to a String cannot fail");
2980
2981    let capability_macro_name = snake_case(&capability_name);
2982    let client_macro_name = snake_case(&format!("{capability_name}Client"));
2983    let operations = contract
2984        .operations
2985        .iter()
2986        .map(|operation| Value::String(operation.name.clone()))
2987        .collect::<Vec<_>>();
2988    let operation_kinds = contract
2989        .operations
2990        .iter()
2991        .filter(|operation| operation.interaction != "request")
2992        .map(|operation| {
2993            (
2994                operation.name.clone(),
2995                Value::String(operation.interaction.clone()),
2996            )
2997        })
2998        .collect::<Map<_, _>>();
2999    let provided_fragment = canonical_json(&serde_json::json!({
3000        "capability_id": contract.capability_id,
3001        "descriptor_version": contract.version,
3002        "operations": operations,
3003        "operation_kinds": operation_kinds,
3004        "default_admission": {
3005            "queue_capacity": 0,
3006            "max_concurrency": 1
3007        },
3008        "operation_admissions": {},
3009        "event_admission": null,
3010        "cross_lane_transfer": contract.cross_lane_transfer
3011    }));
3012    let required_fragment = canonical_json(&serde_json::json!({
3013        "capability_id": contract.capability_id,
3014        "descriptor_version": contract.version,
3015        "cardinality": "one"
3016    }));
3017    writeln!(
3018        output,
3019        "#[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",
3020        quote_string(&provided_fragment),
3021        quote_string(&required_fragment),
3022    )
3023    .expect("writing to a String cannot fail");
3024    for operation in &contract.operations {
3025        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
3026        writeln!(
3027            output,
3028            "pub const {operation_const}: &str = {};",
3029            quote_string(&operation.name)
3030        )
3031        .expect("writing to a String cannot fail");
3032    }
3033    writeln!(
3034        output,
3035        "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
3036        rust_runtime_types(contract).join(", ")
3037    )
3038    .expect("writing to a String cannot fail");
3039    for declaration in types.declarations {
3040        output.push_str(&declaration);
3041        output.push('\n');
3042    }
3043    for marker in operation_markers {
3044        output.push_str(&marker);
3045        output.push('\n');
3046    }
3047    for codec in error_codecs {
3048        output.push_str(&codec);
3049        output.push('\n');
3050    }
3051    for codec in wire_codecs {
3052        output.push_str(&codec);
3053        output.push('\n');
3054    }
3055    for conversion in provider_result_conversions {
3056        output.push_str(&conversion);
3057        output.push('\n');
3058    }
3059    write!(
3060        output,
3061        "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
3062        provider_methods.join("\n")
3063    )
3064    .expect("writing to a String cannot fail");
3065    writeln!(
3066        output,
3067        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_{capability_macro_name} {{\n    ($module:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $module {{\n{}\n        }}\n    }};\n}}\n",
3068        provider_lowering_methods.join("\n")
3069    )
3070    .expect("writing to a String cannot fail");
3071    if has_request_operations {
3072        write!(
3073            output,
3074            "#[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"
3075        )
3076        .expect("writing to a String cannot fail");
3077    } else {
3078        write!(
3079            output,
3080            "#[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"
3081        )
3082        .expect("writing to a String cannot fail");
3083    }
3084    output.push_str(&request_endpoint_impl);
3085    output.push_str(&stream_endpoint_impl);
3086    output.push_str(&event_endpoint_impl);
3087    let request_endpoint_value = has_request_operations.then_some(
3088        "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeRequestEndpoint>",
3089    );
3090    let stream_endpoint_value = has_stream_operations.then_some(
3091        "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeStreamEndpoint>",
3092    );
3093    let event_endpoint_value = has_event_operations
3094        .then_some("endpoint as ::std::rc::Rc<dyn __LensoNativeSupport::NativeEventEndpoint>");
3095    writeln!(
3096        output,
3097        "#[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::NativeModuleInstance::with_all_endpoints(\n            request_endpoints,\n            stream_endpoints,\n            event_endpoints,\n            $lifecycle,\n        )\n    }}}};\n}}\n",
3098        request_endpoint_value.unwrap_or_default(),
3099        stream_endpoint_value.unwrap_or_default(),
3100        event_endpoint_value.unwrap_or_default(),
3101    )
3102    .expect("writing to a String cannot fail");
3103    let new_method = if contract.operations.len() == 1 {
3104        let field = rust_field_name(&contract.operations[0].name);
3105        let marker = &capability_name;
3106        match contract.operations[0].interaction.as_str() {
3107            "request" => format!(
3108                "    pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
3109            ),
3110            "stream" => format!(
3111                "    pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
3112            ),
3113            "event" => format!(
3114                "    pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
3115            ),
3116            _ => unreachable!("Descriptor validation restricts interactions"),
3117        }
3118    } else {
3119        String::new()
3120    };
3121    write!(
3122        output,
3123        "#[derive(Debug)]\npub struct {capability_name}Client {{\n{}\n}}\nimpl {capability_name}Client {{\n{}    pub fn from_dependencies(dependencies: &ModuleDependencies) -> Result<Self, RuntimeFailure> {{\n        <Self as CapabilityClient>::from_dependencies(dependencies)\n    }}\n\n{}\n}}\n\nimpl CapabilityClient for {capability_name}Client {{\n    type Dependencies = ModuleDependencies;\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: &ModuleDependencies) -> Result<Self, RuntimeFailure> {{\n        Ok(Self {{\n{}\n        }})\n    }}\n\n    fn already_connected() -> RuntimeFailure {{\n        RuntimeFailure::ModuleFailure {{\n            detail: format!(\"Capability Port {{CAPABILITY_ID}} was connected more than once\"),\n        }}\n    }}\n}}\n\n",
3124        client_fields.join("\n"),
3125        new_method,
3126        client_methods.join("\n\n"),
3127        client_initializers.join("\n")
3128    )
3129    .expect("writing to a String cannot fail");
3130    for error in invocation_errors {
3131        output.push_str(&error);
3132    }
3133    format!("{}\n", output.trim_end())
3134}
3135
3136fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
3137    let mut types = BTreeSet::from(["UnknownDomainError"]);
3138    for operation in &contract.operations {
3139        collect_rust_runtime_types(&operation.request, &mut types);
3140        collect_rust_runtime_types(&operation.response, &mut types);
3141        for error in &operation.domain_errors {
3142            if let Some(payload) = &error.payload {
3143                if !error.payload_required && payload.is_nullable() {
3144                    types.insert("OptionalValue");
3145                }
3146                collect_rust_runtime_types(payload, &mut types);
3147            }
3148        }
3149    }
3150    types.into_iter().collect()
3151}
3152
3153fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
3154    match ty {
3155        TypeIr::Int64 => {
3156            types.insert("Int64");
3157        }
3158        TypeIr::Uint64 => {
3159            types.insert("Uint64");
3160        }
3161        TypeIr::Bytes => {
3162            types.insert("Bytes");
3163        }
3164        TypeIr::Timestamp => {
3165            types.insert("Timestamp");
3166        }
3167        TypeIr::Duration => {
3168            types.insert("Duration");
3169        }
3170        TypeIr::Array(item) | TypeIr::Nullable(item) => {
3171            collect_rust_runtime_types(item, types);
3172        }
3173        TypeIr::Object { fields, additional } => {
3174            for field in fields {
3175                if !field.required && field.ty.is_nullable() {
3176                    types.insert("OptionalValue");
3177                }
3178                collect_rust_runtime_types(&field.ty, types);
3179            }
3180            if let ObjectAdditionalIr::Typed(value) = additional {
3181                collect_rust_runtime_types(value, types);
3182            }
3183        }
3184        TypeIr::Any
3185        | TypeIr::String
3186        | TypeIr::Integer
3187        | TypeIr::Number
3188        | TypeIr::Boolean
3189        | TypeIr::Null
3190        | TypeIr::Enum(_) => {}
3191    }
3192}
3193
3194#[allow(clippy::too_many_lines)]
3195fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
3196    let mut output = String::new();
3197    writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
3198    output.push_str(
3199        "    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n",
3200    );
3201    output.push_str("        use serde::ser::SerializeMap;\n");
3202    output.push_str("        match self {\n");
3203    for variant in variants {
3204        if variant.structured {
3205            if variant.payload.is_some() {
3206                if variant.payload_required {
3207                    writeln!(
3208                        output,
3209                        "            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            }},",
3210                        variant.name,
3211                        quote_string(&variant.code)
3212                    )
3213                    .expect("String cannot fail");
3214                } else {
3215                    writeln!(
3216                        output,
3217                        "            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            }},",
3218                        variant.name,
3219                        quote_string(&variant.code)
3220                    )
3221                    .expect("String cannot fail");
3222                }
3223            } else {
3224                writeln!(
3225                    output,
3226                    "            Self::{} => {{\n                let mut map = serializer.serialize_map(Some(1))?;\n                map.serialize_entry(\"code\", {})?;\n                map.end()\n            }},",
3227                    variant.name,
3228                    quote_string(&variant.code)
3229                )
3230                .expect("String cannot fail");
3231            }
3232        } else {
3233            writeln!(
3234                output,
3235                "            Self::{} => serializer.serialize_str({}),",
3236                variant.name,
3237                quote_string(&variant.code)
3238            )
3239            .expect("String cannot fail");
3240        }
3241    }
3242    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");
3243    writeln!(
3244        output,
3245        "impl<'de> serde::Deserialize<'de> for {error_name} {{"
3246    )
3247    .expect("String cannot fail");
3248    output.push_str(
3249        "    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",
3250    );
3251    for variant in variants.iter().filter(|variant| !variant.structured) {
3252        writeln!(
3253            output,
3254            "                {} => Ok(Self::{}),",
3255            quote_string(&variant.code),
3256            variant.name
3257        )
3258        .expect("String cannot fail");
3259    }
3260    output.push_str(
3261        "                _ => 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",
3262    );
3263    if variants.iter().any(|variant| variant.structured) {
3264        output.push_str("                match code.as_str() {\n");
3265        for variant in variants.iter().filter(|variant| variant.structured) {
3266            if variant.payload.is_some() {
3267                if variant.payload_required {
3268                    writeln!(
3269                        output,
3270                        "                    {} => {{\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                    }},",
3271                        quote_string(&variant.code),
3272                        variant.name
3273                    )
3274                    .expect("String cannot fail");
3275                } else {
3276                    writeln!(
3277                        output,
3278                        "                    {} => {{\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                    }},",
3279                        quote_string(&variant.code),
3280                        variant.name
3281                    )
3282                    .expect("String cannot fail");
3283                }
3284            } else {
3285                writeln!(
3286                    output,
3287                    "                    {} => Ok(Self::{}),",
3288                    quote_string(&variant.code),
3289                    variant.name
3290                )
3291                .expect("String cannot fail");
3292            }
3293        }
3294        output.push_str(
3295            "                    _ => {\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",
3296        );
3297    } else {
3298        output.push_str(
3299            "                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",
3300        );
3301    }
3302    output.push_str(
3303        "            other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n        }\n    }\n}\n",
3304    );
3305    output
3306}
3307
3308fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
3309    let capability_name = pascal_case(
3310        contract
3311            .capability_id
3312            .split('@')
3313            .next()
3314            .and_then(|identity| identity.rsplit('.').next())
3315            .unwrap_or("Capability"),
3316    );
3317    let codec_name = format!("{capability_name}JsonCodec");
3318    let mut types = RustTypes::new();
3319    let mut operations = Vec::new();
3320    let mut encode_arms = Vec::new();
3321    let mut response_arms = Vec::new();
3322    let mut error_arms = Vec::new();
3323
3324    for operation in &contract.operations {
3325        if operation.interaction != "request" {
3326            return Err(CodegenError::UnsupportedInteraction {
3327                operation: operation.name.clone(),
3328                interaction: operation.interaction.clone(),
3329            });
3330        }
3331        let operation_name = pascal_case(&operation.name);
3332        let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
3333        let response_type =
3334            types.type_for(&operation.response, &format!("{operation_name}Response"));
3335        let error_type = format!("{operation_name}Error");
3336        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
3337        operations.push(operation_const.clone());
3338        encode_arms.push(format!(
3339            "            {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            }}"
3340        ));
3341        response_arms.push(format!(
3342            "            {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()),"
3343        ));
3344        error_arms.push(format!(
3345            "            {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()),"
3346        ));
3347    }
3348
3349    let mut output = generate_rust(contract);
3350    write!(
3351        output,
3352        "\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        &[{}]\n    }}\n\n    fn encode_request(\n        &self,\n        operation: &str,\n        request: &dyn std::any::Any,\n    ) -> Result<serde_json::Value, RuntimeFailure> {{\n        match operation {{\n{},\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}\n    }}\n\n    fn decode_response(\n        &self,\n        operation: &str,\n        value: serde_json::Value,\n    ) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n        match operation {{\n{}\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}\n    }}\n\n    fn decode_domain_error(\n        &self,\n        operation: &str,\n        value: serde_json::Value,\n    ) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n        match operation {{\n{}\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}\n    }}\n}}\n\nfn runtime_codec_protocol_failure() -> RuntimeFailure {{\n    RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }}\n}}\n\nfn runtime_codec_unknown_operation(operation: &str) -> RuntimeFailure {{\n    RuntimeFailure::UnknownOperation {{\n        capability: CAPABILITY_ID,\n        operation: operation.to_owned(),\n    }}\n}}\n",
3353        operations.join(", "),
3354        encode_arms.join(",\n"),
3355        response_arms.join("\n"),
3356        error_arms.join("\n"),
3357    )
3358    .expect("writing generated Rust to a String cannot fail");
3359    Ok(output)
3360}
3361
3362fn generate_rust_wire_codecs(
3363    operation: &str,
3364    request_type: &str,
3365    response_type: &str,
3366    error_type: &str,
3367) -> String {
3368    let stem = snake_case(operation);
3369    format!(
3370        "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"
3371    )
3372}
3373
3374fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
3375    let stem = snake_case(operation);
3376    format!(
3377        "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"
3378    )
3379}
3380
3381fn generate_typescript_codecs(
3382    operation: &str,
3383    request_type: &str,
3384    response_type: &str,
3385    error_type: &str,
3386    variants: &[ErrorVariantIr],
3387) -> String {
3388    let stem = pascal_case(operation);
3389    let known_strings = variants
3390        .iter()
3391        .filter(|variant| !variant.structured)
3392        .map(|variant| quote_string(&variant.code))
3393        .collect::<Vec<_>>()
3394        .join(", ");
3395    format!(
3396        "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"
3397    )
3398}
3399
3400fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
3401    let stem = pascal_case(operation);
3402    format!(
3403        "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"
3404    )
3405}
3406
3407#[allow(clippy::too_many_lines)]
3408fn generate_typescript(contract: &ContractIr) -> String {
3409    let capability_name = pascal_case(
3410        contract
3411            .capability_id
3412            .split('@')
3413            .next()
3414            .and_then(|identity| identity.rsplit('.').next())
3415            .unwrap_or("Capability"),
3416    );
3417    let mut types = TypeScriptTypes::new();
3418    let mut clients = Vec::new();
3419    let mut providers = Vec::new();
3420    let mut errors = Vec::new();
3421    let mut codecs = Vec::new();
3422    let mut request_dispatch_arms = Vec::new();
3423    let operation_names = contract
3424        .operations
3425        .iter()
3426        .map(|operation| quote_string(&operation.name))
3427        .collect::<Vec<_>>()
3428        .join(", ");
3429    let stream_operation_names = contract
3430        .operations
3431        .iter()
3432        .filter(|operation| operation.interaction == "stream")
3433        .map(|operation| quote_string(&operation.name))
3434        .collect::<Vec<_>>()
3435        .join(", ");
3436    let event_operation_names = contract
3437        .operations
3438        .iter()
3439        .filter(|operation| operation.interaction == "event")
3440        .map(|operation| quote_string(&operation.name))
3441        .collect::<Vec<_>>()
3442        .join(", ");
3443    let has_event_operations = contract
3444        .operations
3445        .iter()
3446        .any(|operation| operation.interaction == "event");
3447    for operation in &contract.operations {
3448        let operation_name = pascal_case(&operation.name);
3449        let request_name = format!("{operation_name}Request");
3450        let response_name = format!("{operation_name}Response");
3451        let error_name = format!("{operation_name}Error");
3452        let request_type = types.type_for(&operation.request, &request_name);
3453        let response_type = types.type_for(&operation.response, &response_name);
3454        let variants = &operation.domain_errors;
3455        let error_type = if variants.is_empty() {
3456            "UnknownDomainError".to_owned()
3457        } else {
3458            let mut values = variants
3459                .iter()
3460                .map(|variant| {
3461                    if let Some(payload) = &variant.payload {
3462                        let payload_name = format!("{error_name}{}Payload", variant.name);
3463                        let payload_type =
3464                            types.type_for_non_null(payload.non_null(), &payload_name);
3465                        let payload_type = if payload.is_nullable() {
3466                            format!("{payload_type} | null")
3467                        } else {
3468                            payload_type
3469                        };
3470                        let payload_optional = if variant.payload_required { "" } else { "?" };
3471                        format!(
3472                            "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
3473                            quote_string(&variant.code),
3474                        )
3475                    } else if variant.structured {
3476                        format!("{{ readonly code: {} }}", quote_string(&variant.code))
3477                    } else {
3478                        quote_string(&variant.code)
3479                    }
3480                })
3481                .collect::<Vec<_>>();
3482            values.push("UnknownDomainError".to_owned());
3483            values.join(" | ")
3484        };
3485        let invocation_error_name = format!("{operation_name}InvocationError");
3486        let result_name = format!("{operation_name}Result");
3487        let result_value_type = match operation.interaction.as_str() {
3488            "stream" => format!("StreamSession<{response_type}, {error_name}>"),
3489            "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
3490            _ => response_type.clone(),
3491        };
3492        if operation.interaction == "event" {
3493            errors.push(format!(
3494                "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
3495            ));
3496        } else {
3497            errors.push(format!(
3498                "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} }};"
3499            ));
3500        }
3501        codecs.push(generate_typescript_codecs(
3502            &operation.name,
3503            &request_type,
3504            &response_type,
3505            &error_name,
3506            variants,
3507        ));
3508        if operation.interaction == "event" {
3509            codecs.push(generate_typescript_event_codecs(
3510                &operation.name,
3511                &request_type,
3512            ));
3513        }
3514        if matches!(operation.interaction.as_str(), "request" | "stream") {
3515            clients.push(format!(
3516                "  {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
3517                typescript_property_name(&snake_case(&operation.name)),
3518            ));
3519            providers.push(format!(
3520                "  {}(context: InvocationContext, request: {request_type}): Promise<{result_name}>;",
3521                typescript_property_name(&snake_case(&operation.name)),
3522            ));
3523        } else {
3524            clients.push(format!(
3525                "  {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
3526                typescript_property_name(&snake_case(&operation.name)),
3527            ));
3528            providers.push(format!(
3529                "  {}(context: InvocationContext, event: {request_type}): void;",
3530                typescript_property_name(&snake_case(&operation.name)),
3531            ));
3532        }
3533        if operation.interaction == "request" {
3534            let provider_method = typescript_property_name(&snake_case(&operation.name));
3535            request_dispatch_arms.push(format!(
3536                "      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: \"module_failure\", detail: providerErrorMessage(error) }} }};\n        }}\n      }}",
3537                quote_string(&operation.name),
3538            ));
3539        }
3540    }
3541    let mut output = String::new();
3542    output.push_str(TYPESCRIPT_HEADER);
3543    output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
3544    writeln!(
3545        output,
3546        "export const CAPABILITY_ID = {};",
3547        quote_string(&contract.capability_id)
3548    )
3549    .expect("writing to a String cannot fail");
3550    writeln!(
3551        output,
3552        "export const DESCRIPTOR_VERSION = {};",
3553        quote_string(&contract.version)
3554    )
3555    .expect("writing to a String cannot fail");
3556    writeln!(output, "export const PORTABLE = {};", contract.portable)
3557        .expect("writing to a String cannot fail");
3558    write!(
3559        output,
3560        "export const CROSS_LANE_TRANSFER = {};\n\n",
3561        contract.cross_lane_transfer
3562    )
3563    .expect("writing to a String cannot fail");
3564    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");
3565    if has_event_operations {
3566        output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
3567    }
3568    for declaration in types.declarations {
3569        output.push_str(&declaration);
3570        output.push('\n');
3571    }
3572    for error in errors {
3573        output.push_str(&error);
3574        output.push('\n');
3575    }
3576    for codec in codecs {
3577        output.push_str(&codec);
3578        output.push('\n');
3579    }
3580    write!(
3581        output,
3582        "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
3583        clients.join("\n"),
3584        providers.join("\n")
3585    )
3586    .expect("writing to a String cannot fail");
3587    write!(
3588        output,
3589        "\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",
3590        request_dispatch_arms.join("\n")
3591    )
3592    .expect("writing to a String cannot fail");
3593    output.push_str(
3594        "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
3595    );
3596    output
3597}