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