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