Skip to main content

lenso_contract_codegen/
lib.rs

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