Skip to main content

lenso_contract_codegen/
lib.rs

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