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