Skip to main content

lenso_contract_codegen/
lib.rs

1//! Portable Capability Descriptor validation and binding generation.
2//!
3//! A locked Descriptor and its package-local JSON Schemas are the portable
4//! cross-language authority. They may be derived from compiled source types,
5//! but every backend consumes the same checked snapshot. This crate deliberately
6//! lives above the Kernel: it checks evolution rules and emits deterministic
7//! Rust and TypeScript artifacts before an App is booted.
8
9use std::{
10    collections::{BTreeMap, BTreeSet, HashSet},
11    error::Error,
12    fmt,
13    fmt::Write as _,
14    fs,
15    path::{Path, PathBuf},
16};
17
18use regex::Regex;
19use serde_json::{Map, Value};
20use sha2::{Digest as _, Sha256};
21
22mod browser;
23mod ir;
24mod module_projection;
25mod rust_debug;
26mod source;
27mod wit;
28
29pub use browser::generate_browser_request_client;
30/// Generator cohort recorded by persistent authoring caches.
31pub const GENERATOR_VERSION: &str = env!("CARGO_PKG_VERSION");
32/// Source fingerprint also invalidates caches for unpublished local overlays.
33pub const GENERATOR_REVISION: &str = env!("LENSO_GENERATOR_REVISION");
34pub use module_projection::{
35    check_module_projection, generate_module_projection, write_module_projection,
36};
37pub use source::{check_source_snapshot, write_source_snapshot};
38
39use ir::{ContractIr, ErrorVariantIr, FieldIr, ObjectAdditionalIr, OperationIr, TypeIr};
40
41const GENERATED_HEADER: &str = "// @generated by lenso-contract-codegen; do not edit.\n";
42const TYPESCRIPT_HEADER: &str = "/* @generated by lenso-contract-codegen; do not edit. */\n";
43const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
44
45/// A parsed Descriptor `SemVer`.
46#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
47struct Version(semver::Version);
48
49impl Version {
50    fn parse(value: &str) -> Result<Self, CodegenError> {
51        semver::Version::parse(value)
52            .map(Self)
53            .map_err(|error| CodegenError::InvalidDescriptor {
54                detail: format!("Descriptor version `{value}` is not valid SemVer: {error}"),
55            })
56    }
57
58    const fn major(&self) -> u64 {
59        self.0.major
60    }
61
62    const fn minor(&self) -> u64 {
63        self.0.minor
64    }
65}
66
67fn parse_numeric_component(value: &str) -> Option<u64> {
68    (!value.is_empty() && (value == "0" || !value.starts_with('0')))
69        .then(|| value.parse().ok())
70        .flatten()
71}
72
73#[derive(Clone, Debug)]
74struct Operation {
75    name: String,
76    interaction: String,
77    request_schema: Value,
78    response_schema: Value,
79    domain_error_schema: Value,
80}
81
82fn contract_ir(descriptor: &Descriptor) -> ContractIr {
83    ContractIr {
84        capability_id: descriptor.capability_id.clone(),
85        version: descriptor.version.clone(),
86        descriptor_digest: descriptor.digest.clone(),
87        request_admission: descriptor.request_admission,
88        portable: descriptor.portable,
89        cross_lane_transfer: descriptor.cross_lane_transfer,
90        operations: descriptor
91            .operations
92            .iter()
93            .map(|operation| OperationIr {
94                name: operation.name.clone(),
95                interaction: operation.interaction.clone(),
96                request: type_ir_from_schema(&operation.request_schema),
97                response: type_ir_from_schema(&operation.response_schema),
98                domain_errors: error_variant_ir_definitions(&operation.domain_error_schema),
99            })
100            .collect(),
101    }
102}
103
104fn type_ir_from_schema(schema: &Value) -> TypeIr {
105    if let Some(types) = schema.get("type").and_then(Value::as_array) {
106        let non_null_types = types
107            .iter()
108            .filter(|schema_type| *schema_type != "null")
109            .cloned()
110            .collect::<Vec<_>>();
111        if non_null_types.is_empty() {
112            return TypeIr::Null;
113        }
114        if non_null_types.len() == 1 {
115            let mut narrowed = schema.as_object().cloned().unwrap_or_default();
116            narrowed.insert("type".to_owned(), non_null_types[0].clone());
117            let base = type_ir_non_null(&Value::Object(narrowed));
118            return if non_null_types.len() == types.len() {
119                base
120            } else {
121                TypeIr::Nullable(Box::new(base))
122            };
123        }
124    }
125    type_ir_non_null(schema)
126}
127
128fn type_ir_non_null(schema: &Value) -> TypeIr {
129    let Some(object) = schema.as_object() else {
130        return TypeIr::Any;
131    };
132    if let Some(values) = object.get("enum").and_then(Value::as_array) {
133        return TypeIr::Enum {
134            name: schema_declared_type_name(object),
135            values: values
136                .iter()
137                .filter_map(Value::as_str)
138                .map(ToOwned::to_owned)
139                .collect(),
140        };
141    }
142    if let Some(schema_type) = object.get("type").and_then(Value::as_str) {
143        return match schema_type {
144            "object" => {
145                let required = required_fields(schema);
146                let mut properties = object
147                    .get("properties")
148                    .and_then(Value::as_object)
149                    .into_iter()
150                    .flatten()
151                    .collect::<Vec<_>>();
152                // Projection order is intentionally independent from JSON map
153                // insertion order. Source authoring preserves declaration order
154                // for locked snapshots; language backends retain the historical
155                // canonical name order.
156                properties.sort_unstable_by_key(|(name, _)| *name);
157                let fields = properties
158                    .into_iter()
159                    .map(|(name, schema)| {
160                        let ty = type_ir_from_schema(schema);
161                        let ty = if name.ends_with("_json") && ty == TypeIr::String {
162                            TypeIr::RawJson
163                        } else {
164                            ty
165                        };
166                        FieldIr {
167                            name: name.clone(),
168                            required: required.contains(name),
169                            sensitive: schema
170                                .get("x-lenso-sensitive")
171                                .and_then(Value::as_bool)
172                                .unwrap_or(false),
173                            ty,
174                        }
175                    })
176                    .collect();
177                let additional = match object.get("additionalProperties") {
178                    Some(Value::Bool(false)) => ObjectAdditionalIr::Closed,
179                    Some(Value::Bool(true)) | None => ObjectAdditionalIr::Any,
180                    Some(schema) => {
181                        ObjectAdditionalIr::Typed(Box::new(type_ir_from_schema(schema)))
182                    }
183                };
184                TypeIr::Object {
185                    name: schema_declared_type_name(object),
186                    fields,
187                    additional,
188                }
189            }
190            "array" => object.get("items").map_or(TypeIr::Any, |items| {
191                TypeIr::Array(Box::new(type_ir_from_schema(items)))
192            }),
193            "string" => match object.get("format").and_then(Value::as_str) {
194                Some("int64") => TypeIr::Int64,
195                Some("uint64") => TypeIr::Uint64,
196                Some("byte") => TypeIr::Bytes,
197                Some("date-time") => TypeIr::Timestamp,
198                Some("duration") => TypeIr::Duration,
199                _ => TypeIr::String,
200            },
201            "integer" => TypeIr::Integer,
202            "number" => TypeIr::Number,
203            "boolean" => TypeIr::Boolean,
204            "null" => TypeIr::Null,
205            _ => TypeIr::Any,
206        };
207    }
208    TypeIr::Any
209}
210
211fn schema_declared_type_name(schema: &Map<String, Value>) -> Option<String> {
212    schema
213        .get("title")
214        .and_then(Value::as_str)
215        .filter(|title| !title.is_empty())
216        .map(pascal_case)
217}
218
219/// A validated, self-contained portable Capability Descriptor.
220#[derive(Clone, Debug)]
221pub struct Descriptor {
222    capability_id: String,
223    capability_major: u64,
224    version: String,
225    digest: String,
226    parsed_version: Version,
227    portable: bool,
228    cross_lane_transfer: bool,
229    request_admission: Option<(u32, u32)>,
230    operations: Vec<Operation>,
231}
232
233impl Descriptor {
234    /// Returns the stable Capability series identity, including its major.
235    #[must_use]
236    pub fn capability_id(&self) -> &str {
237        &self.capability_id
238    }
239
240    /// Returns the major encoded in the Capability identity.
241    #[must_use]
242    pub const fn capability_major(&self) -> u64 {
243        self.capability_major
244    }
245
246    /// Returns the exact Descriptor `SemVer` selected for generation.
247    #[must_use]
248    pub fn version(&self) -> &str {
249        &self.version
250    }
251
252    /// Returns the canonical digest of the resolved Descriptor and Schemas.
253    #[must_use]
254    pub fn descriptor_digest(&self) -> &str {
255        &self.digest
256    }
257
258    /// Returns whether the Capability is intended to cross Runtime Adapters.
259    #[must_use]
260    pub const fn portable(&self) -> bool {
261        self.portable
262    }
263
264    /// Returns whether generated native values support transfer across Execution Lanes.
265    #[must_use]
266    pub const fn cross_lane_transfer(&self) -> bool {
267        self.cross_lane_transfer
268    }
269
270    /// Returns the stable Operation names in deterministic lexical order.
271    #[must_use]
272    pub fn operation_names(&self) -> Vec<&str> {
273        self.operations
274            .iter()
275            .map(|operation| operation.name.as_str())
276            .collect()
277    }
278}
279
280/// Metadata embedded in every generated language artifact.
281#[derive(Clone, Debug, Eq, PartialEq)]
282pub struct GeneratedMetadata {
283    /// Stable `namespace.name@major` identity.
284    pub capability_id: String,
285    /// Exact Descriptor `SemVer`.
286    pub descriptor_version: String,
287    /// Canonical SHA-256 digest of the resolved Descriptor and Schemas.
288    pub descriptor_digest: String,
289    /// Whether the source Descriptor is portable.
290    pub portable: bool,
291    /// Whether generated native values may transfer across Execution Lanes.
292    pub cross_lane_transfer: bool,
293}
294
295/// One language projection emitted from a portable Capability contract.
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub enum ProjectionLanguage {
298    /// Native Rust value, consumer, and provider bindings.
299    Rust,
300    /// TypeScript value, consumer, and provider bindings.
301    TypeScript,
302    /// WebAssembly Component Model WIT provider and consumer worlds.
303    Wit,
304    /// Rust bindings plus the generated byte-Adapter Capability codec.
305    RustRuntime,
306    /// Portable Rust values and a typed Process Plugin dependency client.
307    RustPlugin,
308}
309
310/// One independently generated language projection.
311#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct GeneratedProjection {
313    /// Metadata embedded in the generated projection.
314    pub metadata: GeneratedMetadata,
315    /// Selected target language.
316    pub language: ProjectionLanguage,
317    /// Generated provider, client, value, and error bindings.
318    pub source: String,
319}
320
321/// Compatibility aggregate for callers that intentionally generate both projections.
322#[derive(Clone, Debug, Eq, PartialEq)]
323pub struct GeneratedArtifacts {
324    /// Metadata shared by all generated languages.
325    pub metadata: GeneratedMetadata,
326    /// Rust provider, client, value, and error bindings.
327    pub rust: String,
328    /// TypeScript provider, client, value, and error bindings.
329    pub typescript: String,
330}
331
332/// Errors produced while parsing, validating, or generating a contract.
333#[derive(Debug)]
334pub enum CodegenError {
335    /// A file could not be read.
336    Io {
337        path: PathBuf,
338        source: std::io::Error,
339    },
340    /// Descriptor or Schema data is malformed.
341    InvalidDescriptor { detail: String },
342    /// A Schema cannot be used by the portable value profile.
343    UnsupportedSchema { path: PathBuf, detail: String },
344    /// A Descriptor interaction is not supported by the generated language bindings.
345    UnsupportedInteraction {
346        operation: String,
347        interaction: String,
348    },
349    /// A portable JSON Schema shape has no exact Component Model representation.
350    UnsupportedWit { detail: String },
351    /// A JSON value would be lossy when represented by JavaScript numbers.
352    InvalidPortableValue { path: String, detail: String },
353    /// A checked-in generated artifact differs from its source.
354    GeneratedArtifactDrift { path: PathBuf },
355}
356
357impl fmt::Display for CodegenError {
358    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
359        match self {
360            Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
361            Self::InvalidDescriptor { detail } => {
362                write!(formatter, "invalid Descriptor: {detail}")
363            }
364            Self::UnsupportedSchema { path, detail } => {
365                write!(formatter, "unsupported Schema {}: {detail}", path.display())
366            }
367            Self::UnsupportedInteraction {
368                operation,
369                interaction,
370            } => write!(
371                formatter,
372                "Operation `{operation}` uses unsupported interaction `{interaction}`"
373            ),
374            Self::UnsupportedWit { detail } => {
375                write!(formatter, "unsupported WIT projection: {detail}")
376            }
377            Self::InvalidPortableValue { path, detail } => {
378                write!(formatter, "invalid portable value at `{path}`: {detail}")
379            }
380            Self::GeneratedArtifactDrift { path } => {
381                write!(formatter, "generated artifact is stale: {}", path.display())
382            }
383        }
384    }
385}
386
387impl Error for CodegenError {
388    fn source(&self) -> Option<&(dyn Error + 'static)> {
389        match self {
390            Self::Io { source, .. } => Some(source),
391            _ => None,
392        }
393    }
394}
395
396/// Reasons why a Descriptor evolution is not safe for an existing Capability
397/// series.
398#[derive(Clone, Debug, Eq, PartialEq)]
399pub enum CompatibilityError {
400    /// The two files do not describe the same Capability series.
401    IdentityChanged { from: String, to: String },
402    /// The new Descriptor is not newer than the old one.
403    VersionNotAdvanced { from: String, to: String },
404    /// The change is not allowed by the old series and requires a new major.
405    BreakingChanges { changes: Vec<String> },
406}
407
408impl fmt::Display for CompatibilityError {
409    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self {
411            Self::IdentityChanged { from, to } => {
412                write!(
413                    formatter,
414                    "Capability identity changed from `{from}` to `{to}`"
415                )
416            }
417            Self::VersionNotAdvanced { from, to } => {
418                write!(
419                    formatter,
420                    "Descriptor version must advance from `{from}` to `{to}`"
421                )
422            }
423            Self::BreakingChanges { changes } => write!(
424                formatter,
425                "breaking Descriptor changes require a new major: {}",
426                changes.join("; ")
427            ),
428        }
429    }
430}
431
432impl Error for CompatibilityError {}
433
434/// Reads and validates one Descriptor, resolving package-local `$ref` files.
435#[allow(clippy::too_many_lines)]
436pub fn load_descriptor(path: &Path) -> Result<Descriptor, CodegenError> {
437    let descriptor_path = canonical_path(path)?;
438    let package_root = descriptor_path
439        .parent()
440        .unwrap_or_else(|| Path::new("."))
441        .to_path_buf();
442    let descriptor_value = read_json(&descriptor_path)?;
443    let object = descriptor_value
444        .as_object()
445        .ok_or_else(|| CodegenError::InvalidDescriptor {
446            detail: "the Descriptor root must be an object".to_owned(),
447        })?;
448
449    let capability_id = required_string(object, "id")?;
450    let (identity, identity_major) = capability_identity(&capability_id)?;
451    let version = required_string(object, "version")?;
452    let parsed_version = Version::parse(&version)?;
453    let portable = object
454        .get("portable")
455        .and_then(Value::as_bool)
456        .unwrap_or(false);
457    let cross_lane_transfer = object
458        .get("cross_lane_transfer")
459        .and_then(Value::as_bool)
460        .unwrap_or(false);
461    let request_admission = request_admission(object.get("request_admission"))?;
462    let operation_values = object
463        .get("operations")
464        .and_then(Value::as_array)
465        .ok_or_else(|| CodegenError::InvalidDescriptor {
466            detail: "`operations` must be an array".to_owned(),
467        })?;
468    if operation_values.is_empty() {
469        return Err(CodegenError::InvalidDescriptor {
470            detail: "a Descriptor must declare at least one Operation".to_owned(),
471        });
472    }
473
474    let mut operations = Vec::with_capacity(operation_values.len());
475    let mut operation_names = BTreeSet::new();
476    let mut generated_operation_names = BTreeSet::new();
477    let mut generated_type_names = BTreeSet::new();
478    let mut generated_client_method_names = BTreeSet::from([
479        "new".to_owned(),
480        "from_context".to_owned(),
481        "from_dependencies".to_owned(),
482    ]);
483    let capability_name = identity
484        .rsplit('.')
485        .next()
486        .map_or_else(|| "Capability".to_owned(), pascal_case);
487    for operation_value in operation_values {
488        let operation =
489            operation_value
490                .as_object()
491                .ok_or_else(|| CodegenError::InvalidDescriptor {
492                    detail: "each Operation must be an object".to_owned(),
493                })?;
494        let name = required_string(operation, "name")?;
495        if !is_identifier(&name) || !is_rust_member_name(&name) {
496            return Err(CodegenError::InvalidDescriptor {
497                detail: format!(
498                    "Operation name `{name}` must start with a letter or `_` and contain only letters, digits, `_`, or `-`"
499                ),
500            });
501        }
502        if !operation_names.insert(name.clone()) {
503            return Err(CodegenError::InvalidDescriptor {
504                detail: format!("Operation `{name}` is declared more than once"),
505            });
506        }
507        for generated_name in [rust_field_name(&name), pascal_case(&name)] {
508            if !generated_operation_names.insert(generated_name.clone()) {
509                return Err(CodegenError::InvalidDescriptor {
510                    detail: format!(
511                        "Operation name `{name}` collides after code generation as `{generated_name}`"
512                    ),
513                });
514            }
515        }
516        let operation_name = pascal_case(&name);
517        let mut generated_names = vec![
518            format!("{operation_name}Request"),
519            format!("{operation_name}Response"),
520            format!("{operation_name}Error"),
521            format!("{operation_name}InvocationError"),
522            format!("{operation_name}Result"),
523            format!("{capability_name}{operation_name}"),
524        ];
525        generated_names.push(if operation_values.len() == 1 {
526            format!("{capability_name}InvocationError")
527        } else {
528            format!("{capability_name}{operation_name}InvocationError")
529        });
530        for generated_name in generated_names {
531            if !is_generated_type_name(&generated_name)
532                || !generated_type_names.insert(generated_name.clone())
533            {
534                return Err(CodegenError::InvalidDescriptor {
535                    detail: format!(
536                        "Operation name `{name}` collides after type generation as `{generated_name}`"
537                    ),
538                });
539            }
540        }
541        let interaction = required_string(operation, "interaction")?;
542        if !matches!(interaction.as_str(), "request" | "stream" | "event") {
543            return Err(CodegenError::InvalidDescriptor {
544                detail: format!("Operation `{name}` has unsupported interaction `{interaction}`"),
545            });
546        }
547        if matches!(interaction.as_str(), "request" | "stream" | "event") {
548            let client_method_name = rust_field_name(&name);
549            for generated_name in [
550                client_method_name.clone(),
551                format!("{client_method_name}_with_context"),
552            ] {
553                if !generated_client_method_names.insert(generated_name.clone()) {
554                    return Err(CodegenError::InvalidDescriptor {
555                        detail: format!(
556                            "Operation name `{name}` collides with the generated Client API as `{generated_name}`"
557                        ),
558                    });
559                }
560            }
561        }
562        let request_schema_path =
563            schema_path(&descriptor_path, operation, "request_schema", &name)?;
564        let response_schema_path =
565            schema_path(&descriptor_path, operation, "response_schema", &name)?;
566        let domain_error_schema_path =
567            schema_path(&descriptor_path, operation, "domain_error_schema", &name)?;
568        let mut ref_stack = Vec::new();
569        let request_source = read_json(&request_schema_path)?;
570        let mut request_schema = resolve_refs(
571            &request_source,
572            &request_source,
573            &request_schema_path,
574            &package_root,
575            &mut ref_stack,
576        )?;
577        normalize_nullable_unions(&mut request_schema);
578        validate_schema_profile(&request_schema, &request_schema_path)?;
579        validate_value_generation_schema(&request_schema, &request_schema_path)?;
580        let mut ref_stack = Vec::new();
581        let response_source = read_json(&response_schema_path)?;
582        let mut response_schema = resolve_refs(
583            &response_source,
584            &response_source,
585            &response_schema_path,
586            &package_root,
587            &mut ref_stack,
588        )?;
589        normalize_nullable_unions(&mut response_schema);
590        validate_schema_profile(&response_schema, &response_schema_path)?;
591        validate_value_generation_schema(&response_schema, &response_schema_path)?;
592        let mut ref_stack = Vec::new();
593        let domain_error_source = read_json(&domain_error_schema_path)?;
594        let mut domain_error_schema = resolve_refs(
595            &domain_error_source,
596            &domain_error_source,
597            &domain_error_schema_path,
598            &package_root,
599            &mut ref_stack,
600        )?;
601        normalize_nullable_unions(&mut domain_error_schema);
602        validate_schema_profile(&domain_error_schema, &domain_error_schema_path)?;
603        validate_domain_error_schema(&domain_error_schema, &domain_error_schema_path)?;
604        operations.push(Operation {
605            name,
606            interaction,
607            request_schema,
608            response_schema,
609            domain_error_schema,
610        });
611    }
612    operations.sort_by(|left, right| left.name.cmp(&right.name));
613
614    let capability_id = format!("{identity}@{identity_major}");
615    let descriptor_digest = resolved_descriptor_digest(
616        &capability_id,
617        &version,
618        portable,
619        cross_lane_transfer,
620        request_admission,
621        &operations,
622    );
623    Ok(Descriptor {
624        capability_id,
625        capability_major: identity_major,
626        version,
627        digest: descriptor_digest,
628        parsed_version,
629        portable,
630        cross_lane_transfer,
631        request_admission,
632        operations,
633    })
634}
635
636fn resolved_descriptor_digest(
637    capability_id: &str,
638    version: &str,
639    portable: bool,
640    cross_lane_transfer: bool,
641    request_admission: Option<(u32, u32)>,
642    operations: &[Operation],
643) -> String {
644    let operations = operations
645        .iter()
646        .map(|operation| {
647            serde_json::json!({
648                "name": operation.name,
649                "interaction": operation.interaction,
650                "request_schema": operation.request_schema,
651                "response_schema": operation.response_schema,
652                "domain_error_schema": operation.domain_error_schema,
653            })
654        })
655        .collect::<Vec<_>>();
656    let mut value = serde_json::json!({
657        "capability_id": capability_id,
658        "version": version,
659        "portable": portable,
660        "cross_lane_transfer": cross_lane_transfer,
661        "operations": operations,
662    });
663    if let Some((queue_capacity, max_concurrency)) = request_admission {
664        value["request_admission"] = serde_json::json!({
665            "queue_capacity": queue_capacity,
666            "max_concurrency": max_concurrency,
667        });
668    }
669    let canonical = canonicalize_json(&value);
670    format!("sha256:{:x}", Sha256::digest(canonical.as_bytes()))
671}
672
673fn canonicalize_json(value: &Value) -> String {
674    match value {
675        Value::Array(values) => format!(
676            "[{}]",
677            values
678                .iter()
679                .map(canonicalize_json)
680                .collect::<Vec<_>>()
681                .join(",")
682        ),
683        Value::Object(object) => {
684            let mut keys = object.keys().collect::<Vec<_>>();
685            keys.sort();
686            format!(
687                "{{{}}}",
688                keys.into_iter()
689                    .map(|key| format!(
690                        "{}:{}",
691                        serde_json::to_string(key).expect("object keys serialize"),
692                        canonicalize_json(&object[key])
693                    ))
694                    .collect::<Vec<_>>()
695                    .join(",")
696            )
697        }
698        _ => serde_json::to_string(value).expect("JSON values serialize"),
699    }
700}
701
702fn request_admission(value: Option<&Value>) -> Result<Option<(u32, u32)>, CodegenError> {
703    let Some(value) = value else {
704        return Ok(None);
705    };
706    let invalid = || {
707        CodegenError::InvalidDescriptor {
708        detail: "request_admission requires only queue_capacity (u32) and max_concurrency (positive u32)".to_owned(),
709    }
710    };
711    let object = value.as_object().ok_or_else(invalid)?;
712    if object.len() != 2 {
713        return Err(invalid());
714    }
715    let queue = object
716        .get("queue_capacity")
717        .and_then(Value::as_u64)
718        .and_then(|number| u32::try_from(number).ok())
719        .ok_or_else(invalid)?;
720    let concurrency = object
721        .get("max_concurrency")
722        .and_then(Value::as_u64)
723        .and_then(|number| u32::try_from(number).ok())
724        .filter(|number| *number > 0)
725        .ok_or_else(invalid)?;
726    Ok(Some((queue, concurrency)))
727}
728
729fn required_string(object: &Map<String, Value>, key: &str) -> Result<String, CodegenError> {
730    object
731        .get(key)
732        .and_then(Value::as_str)
733        .filter(|value| !value.is_empty())
734        .map(ToOwned::to_owned)
735        .ok_or_else(|| CodegenError::InvalidDescriptor {
736            detail: format!("`{key}` must be a non-empty string"),
737        })
738}
739
740fn capability_identity(value: &str) -> Result<(String, u64), CodegenError> {
741    let Some((identity, major)) = value.rsplit_once('@') else {
742        return Err(CodegenError::InvalidDescriptor {
743            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
744        });
745    };
746    let generated_name = identity
747        .rsplit('.')
748        .next()
749        .map(pascal_case)
750        .unwrap_or_default();
751    if identity.is_empty()
752        || identity.split('.').count() < 2
753        || !identity.split('.').all(is_identifier)
754        || !is_generated_type_name(&generated_name)
755    {
756        return Err(CodegenError::InvalidDescriptor {
757            detail: format!("Capability id `{value}` must use `namespace.name@major`"),
758        });
759    }
760    let Some(major) = parse_numeric_component(major) else {
761        return Err(CodegenError::InvalidDescriptor {
762            detail: format!("Capability id `{value}` has an invalid major"),
763        });
764    };
765    Ok((identity.to_owned(), major))
766}
767
768fn schema_path(
769    descriptor_path: &Path,
770    operation: &Map<String, Value>,
771    key: &str,
772    operation_name: &str,
773) -> Result<PathBuf, CodegenError> {
774    let value = required_string(operation, key).map_err(|error| match error {
775        CodegenError::InvalidDescriptor { detail } => CodegenError::InvalidDescriptor {
776            detail: format!("Operation `{operation_name}`: {detail}"),
777        },
778        other => other,
779    })?;
780    let candidate = descriptor_path
781        .parent()
782        .unwrap_or_else(|| Path::new("."))
783        .join(value);
784    let canonical = canonical_path(&candidate)?;
785    let package_root = canonical_path(descriptor_path.parent().unwrap_or_else(|| Path::new(".")))?;
786    if !canonical.starts_with(&package_root) {
787        return Err(CodegenError::UnsupportedSchema {
788            path: canonical,
789            detail: "Schema paths must remain inside the Descriptor package".to_owned(),
790        });
791    }
792    Ok(canonical)
793}
794
795fn canonical_path(path: &Path) -> Result<PathBuf, CodegenError> {
796    fs::canonicalize(path).map_err(|source| CodegenError::Io {
797        path: path.to_path_buf(),
798        source,
799    })
800}
801
802fn read_json(path: &Path) -> Result<Value, CodegenError> {
803    let source = fs::read_to_string(path).map_err(|source| CodegenError::Io {
804        path: path.to_path_buf(),
805        source,
806    })?;
807    serde_json::from_str(&source).map_err(|error| CodegenError::InvalidDescriptor {
808        detail: format!("{}: {error}", path.display()),
809    })
810}
811
812fn resolve_refs(
813    value: &Value,
814    root: &Value,
815    source_path: &Path,
816    package_root: &Path,
817    stack: &mut Vec<(PathBuf, String)>,
818) -> Result<Value, CodegenError> {
819    match value {
820        Value::Object(object) => {
821            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
822                let target_value = if reference.starts_with('#') {
823                    let fragment = reference.strip_prefix('#').unwrap_or_default();
824                    let target_path = source_path.to_path_buf();
825                    let key = (target_path.clone(), reference.to_owned());
826                    if stack.contains(&key) {
827                        return Err(CodegenError::UnsupportedSchema {
828                            path: source_path.to_path_buf(),
829                            detail: format!("cyclic local Schema `$ref` `{reference}`"),
830                        });
831                    }
832                    let target = json_pointer(root, fragment).ok_or_else(|| {
833                        CodegenError::UnsupportedSchema {
834                            path: source_path.to_path_buf(),
835                            detail: format!(
836                                "local JSON Pointer `$ref` `{reference}` was not found"
837                            ),
838                        }
839                    })?;
840                    stack.push(key);
841                    let resolved = resolve_refs(target, root, source_path, package_root, stack)?;
842                    stack.pop();
843                    resolved
844                } else {
845                    let (reference_path, fragment) =
846                        reference.split_once('#').unwrap_or((reference, ""));
847                    let target_path = source_path
848                        .parent()
849                        .unwrap_or_else(|| Path::new("."))
850                        .join(reference_path);
851                    let target_path = canonical_path(&target_path)?;
852                    if !target_path.starts_with(package_root) {
853                        return Err(CodegenError::UnsupportedSchema {
854                            path: source_path.to_path_buf(),
855                            detail: format!(
856                                "external Schema `$ref` `{reference}` leaves the Descriptor package"
857                            ),
858                        });
859                    }
860                    let key = (target_path.clone(), format!("#{fragment}"));
861                    if stack.contains(&key) {
862                        return Err(CodegenError::UnsupportedSchema {
863                            path: source_path.to_path_buf(),
864                            detail: format!(
865                                "cyclic Schema `$ref` through {}",
866                                target_path.display()
867                            ),
868                        });
869                    }
870                    let target_root = read_json(&target_path)?;
871                    let target = json_pointer(&target_root, fragment).ok_or_else(|| {
872                        CodegenError::UnsupportedSchema {
873                            path: source_path.to_path_buf(),
874                            detail: format!(
875                                "external JSON Pointer `$ref` `{reference}` was not found"
876                            ),
877                        }
878                    })?;
879                    stack.push(key);
880                    let resolved =
881                        resolve_refs(target, &target_root, &target_path, package_root, stack)?;
882                    stack.pop();
883                    resolved
884                };
885                let mut merged = target_value.as_object().cloned().ok_or_else(|| {
886                    CodegenError::UnsupportedSchema {
887                        path: source_path.to_path_buf(),
888                        detail: "a `$ref` target must resolve to an object Schema".to_owned(),
889                    }
890                })?;
891                restore_reference_type_name(&mut merged, reference);
892                for (key, child) in object {
893                    if key != "$ref" {
894                        merged.insert(
895                            key.clone(),
896                            resolve_refs(child, root, source_path, package_root, stack)?,
897                        );
898                    }
899                }
900                return Ok(Value::Object(merged));
901            }
902            let mut resolved = Map::new();
903            for (key, child) in object {
904                resolved.insert(
905                    key.clone(),
906                    resolve_refs(child, root, source_path, package_root, stack)?,
907                );
908            }
909            Ok(Value::Object(resolved))
910        }
911        Value::Array(values) => values
912            .iter()
913            .map(|child| resolve_refs(child, root, source_path, package_root, stack))
914            .collect::<Result<Vec<_>, _>>()
915            .map(Value::Array),
916        _ => Ok(value.clone()),
917    }
918}
919
920fn reference_definition_name(reference: &str) -> Option<String> {
921    let (_, fragment) = reference.rsplit_once('#')?;
922    let name = fragment.strip_prefix("/$defs/")?;
923    if name.is_empty() || name.contains('/') {
924        return None;
925    }
926    Some(name.replace("~1", "/").replace("~0", "~"))
927}
928
929fn restore_reference_type_name(schema: &mut Map<String, Value>, reference: &str) {
930    if schema.get("title").is_none()
931        && let Some(name) = reference_definition_name(reference)
932    {
933        schema.insert("title".to_owned(), Value::String(name));
934    }
935}
936
937fn json_pointer<'a>(root: &'a Value, fragment: &str) -> Option<&'a Value> {
938    if fragment.is_empty() {
939        return Some(root);
940    }
941    let pointer = fragment.strip_prefix('/')?;
942    let mut current = root;
943    for token in pointer.split('/') {
944        let token = token.replace("~1", "/").replace("~0", "~");
945        current = match current {
946            Value::Object(object) => object.get(&token)?,
947            Value::Array(array) => array.get(token.parse::<usize>().ok()?)?,
948            _ => return None,
949        };
950    }
951    Some(current)
952}
953
954fn normalize_nullable_unions(value: &mut Value) {
955    match value {
956        Value::Object(object) => {
957            for child in object.values_mut() {
958                normalize_nullable_unions(child);
959            }
960            let nullable = ["anyOf", "oneOf"].into_iter().find_map(|key| {
961                let alternatives = object.get(key)?.as_array()?;
962                if alternatives.len() != 2 {
963                    return None;
964                }
965                let null_index = alternatives.iter().position(|alternative| {
966                    alternative.get("type").and_then(Value::as_str) == Some("null")
967                })?;
968                let non_null = alternatives.get(1 - null_index)?.as_object()?.clone();
969                let schema_type = non_null
970                    .get("type")
971                    .and_then(Value::as_str)
972                    .map(ToOwned::to_owned)?;
973                Some((key, schema_type, non_null))
974            });
975            if let Some((union_key, schema_type, mut normalized)) = nullable {
976                normalized.insert(
977                    "type".to_owned(),
978                    Value::Array(vec![
979                        Value::String(schema_type),
980                        Value::String("null".to_owned()),
981                    ]),
982                );
983                for (key, child) in std::mem::take(object) {
984                    if key != union_key {
985                        normalized.entry(key).or_insert(child);
986                    }
987                }
988                *object = normalized;
989            }
990        }
991        Value::Array(values) => {
992            for child in values {
993                normalize_nullable_unions(child);
994            }
995        }
996        _ => {}
997    }
998}
999
1000const SUPPORTED_SCHEMA_KEYWORDS: &[&str] = &[
1001    "$defs",
1002    "additionalProperties",
1003    "anyOf",
1004    "const",
1005    "else",
1006    "enum",
1007    "exclusiveMaximum",
1008    "exclusiveMinimum",
1009    "format",
1010    "if",
1011    "items",
1012    "maxItems",
1013    "maxLength",
1014    "maxProperties",
1015    "maximum",
1016    "minItems",
1017    "minLength",
1018    "minProperties",
1019    "minimum",
1020    "oneOf",
1021    "pattern",
1022    "properties",
1023    "propertyNames",
1024    "required",
1025    "then",
1026    "type",
1027    "uniqueItems",
1028];
1029
1030const HARMLESS_SCHEMA_ANNOTATIONS: &[&str] = &[
1031    "$anchor",
1032    "$comment",
1033    "$dynamicAnchor",
1034    "$id",
1035    "$schema",
1036    "default",
1037    "deprecated",
1038    "description",
1039    "examples",
1040    "readOnly",
1041    "title",
1042    "writeOnly",
1043    "x-lenso-max-container-items",
1044    "x-lenso-max-depth",
1045    "x-lenso-max-encoded-bytes",
1046    "x-lenso-max-nodes",
1047    "x-lenso-max-string-length",
1048    "x-lenso-sensitive",
1049];
1050
1051const PORTABLE_SCHEMA_TYPES: &[&str] = &[
1052    "array", "boolean", "integer", "null", "number", "object", "string",
1053];
1054
1055const PORTABLE_STRING_FORMATS: &[&str] = &["byte", "date-time", "duration", "int64", "uint64"];
1056const MAX_PORTABLE_PATTERN_CHARS: usize = 4_096;
1057const MAX_PORTABLE_REPETITION: u32 = 10_000;
1058const PORTABLE_UNICODE_PROPERTY_ESCAPE: &str = r"\p{Letter}";
1059
1060fn validate_schema_profile(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1061    let Some(object) = schema.as_object() else {
1062        return Err(unsupported_schema(
1063            source_path,
1064            "a JSON Schema must be an object",
1065        ));
1066    };
1067
1068    for keyword in object.keys() {
1069        if !SUPPORTED_SCHEMA_KEYWORDS.contains(&keyword.as_str())
1070            && !HARMLESS_SCHEMA_ANNOTATIONS.contains(&keyword.as_str())
1071        {
1072            return Err(unsupported_schema(
1073                source_path,
1074                format!("Schema keyword `{keyword}` is outside the portable profile"),
1075            ));
1076        }
1077    }
1078
1079    validate_schema_type_keyword(object, source_path)?;
1080    validate_schema_composition_keywords(object, source_path)?;
1081    validate_schema_scalar_keywords(object, source_path)?;
1082    validate_schema_numeric_keywords(object, source_path)?;
1083    validate_schema_children(object, source_path)
1084}
1085
1086fn validate_schema_type_keyword(
1087    object: &Map<String, Value>,
1088    source_path: &Path,
1089) -> Result<(), CodegenError> {
1090    if let Some(schema_type) = object.get("type") {
1091        match schema_type {
1092            Value::String(schema_type) => validate_schema_type(schema_type, source_path)?,
1093            Value::Array(types) if !types.is_empty() => {
1094                let mut unique = BTreeSet::new();
1095                for schema_type in types {
1096                    let Some(schema_type) = schema_type.as_str() else {
1097                        return Err(unsupported_schema(
1098                            source_path,
1099                            "Schema type unions must contain strings",
1100                        ));
1101                    };
1102                    validate_schema_type(schema_type, source_path)?;
1103                    if !unique.insert(schema_type) {
1104                        return Err(unsupported_schema(
1105                            source_path,
1106                            "Schema type unions must be unique",
1107                        ));
1108                    }
1109                }
1110            }
1111            Value::Array(_) => {
1112                return Err(unsupported_schema(
1113                    source_path,
1114                    "a Schema type union cannot be empty",
1115                ));
1116            }
1117            _ => {
1118                return Err(unsupported_schema(
1119                    source_path,
1120                    "Schema `type` must be a string or array",
1121                ));
1122            }
1123        }
1124    }
1125
1126    Ok(())
1127}
1128
1129fn validate_schema_composition_keywords(
1130    object: &Map<String, Value>,
1131    source_path: &Path,
1132) -> Result<(), CodegenError> {
1133    for keyword in ["oneOf", "anyOf"] {
1134        if let Some(value) = object.get(keyword) {
1135            let Some(alternatives) = value.as_array().filter(|values| !values.is_empty()) else {
1136                return Err(unsupported_schema(
1137                    source_path,
1138                    format!("Schema `{keyword}` must be a non-empty array of Schemas"),
1139                ));
1140            };
1141            for alternative in alternatives {
1142                validate_schema_profile(alternative, source_path)?;
1143            }
1144        }
1145    }
1146
1147    Ok(())
1148}
1149
1150fn validate_schema_scalar_keywords(
1151    object: &Map<String, Value>,
1152    source_path: &Path,
1153) -> Result<(), CodegenError> {
1154    if let Some(values) = object.get("enum")
1155        && values.as_array().is_none_or(Vec::is_empty)
1156    {
1157        return Err(unsupported_schema(
1158            source_path,
1159            "Schema `enum` must be a non-empty array",
1160        ));
1161    }
1162    if let Some(required) = object.get("required")
1163        && required
1164            .as_array()
1165            .is_none_or(|values| values.iter().any(|value| !value.is_string()))
1166    {
1167        return Err(unsupported_schema(
1168            source_path,
1169            "Schema `required` must be an array of strings",
1170        ));
1171    }
1172    if let Some(pattern) = object.get("pattern") {
1173        let Some(pattern) = pattern.as_str() else {
1174            return Err(unsupported_schema(
1175                source_path,
1176                "Schema `pattern` must be a string",
1177            ));
1178        };
1179        compile_portable_pattern(pattern).map_err(|error| {
1180            unsupported_schema(
1181                source_path,
1182                format!("Schema `pattern` is outside the portable regex subset: {error}"),
1183            )
1184        })?;
1185    }
1186    if let Some(format) = object.get("format") {
1187        let Some(format) = format.as_str() else {
1188            return Err(unsupported_schema(
1189                source_path,
1190                "Schema `format` must be a string",
1191            ));
1192        };
1193        if !PORTABLE_STRING_FORMATS.contains(&format) {
1194            return Err(unsupported_schema(
1195                source_path,
1196                format!("format `{format}` is outside the portable value profile"),
1197            ));
1198        }
1199    }
1200    if object
1201        .get("uniqueItems")
1202        .is_some_and(|value| !value.is_boolean())
1203    {
1204        return Err(unsupported_schema(
1205            source_path,
1206            "Schema `uniqueItems` must be a boolean",
1207        ));
1208    }
1209
1210    Ok(())
1211}
1212
1213fn validate_schema_numeric_keywords(
1214    object: &Map<String, Value>,
1215    source_path: &Path,
1216) -> Result<(), CodegenError> {
1217    for keyword in [
1218        "minLength",
1219        "maxLength",
1220        "minItems",
1221        "maxItems",
1222        "minProperties",
1223        "maxProperties",
1224    ] {
1225        if let Some(value) = object.get(keyword)
1226            && non_negative_safe_schema_integer(value).is_none()
1227        {
1228            return Err(unsupported_schema(
1229                source_path,
1230                format!("Schema `{keyword}` must be a non-negative safe integer"),
1231            ));
1232        }
1233    }
1234    for keyword in ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"] {
1235        if let Some(value) = object.get(keyword)
1236            && value.as_f64().is_none_or(|number| !number.is_finite())
1237        {
1238            return Err(unsupported_schema(
1239                source_path,
1240                format!("Schema `{keyword}` must be a finite number"),
1241            ));
1242        }
1243    }
1244
1245    Ok(())
1246}
1247
1248fn validate_schema_children(
1249    object: &Map<String, Value>,
1250    source_path: &Path,
1251) -> Result<(), CodegenError> {
1252    if let Some(items) = object.get("items") {
1253        validate_schema_profile(items, source_path)?;
1254    }
1255    for keyword in ["properties", "$defs"] {
1256        if let Some(value) = object.get(keyword) {
1257            let Some(schemas) = value.as_object() else {
1258                return Err(unsupported_schema(
1259                    source_path,
1260                    format!("Schema `{keyword}` must be an object of Schemas"),
1261                ));
1262            };
1263            for child in schemas.values() {
1264                validate_schema_profile(child, source_path)?;
1265            }
1266        }
1267    }
1268    if let Some(additional) = object.get("additionalProperties")
1269        && !additional.is_boolean()
1270    {
1271        validate_schema_profile(additional, source_path)?;
1272    }
1273    if let Some(property_names) = object.get("propertyNames") {
1274        validate_schema_profile(property_names, source_path)?;
1275    }
1276    if object.contains_key("then") || object.contains_key("else") {
1277        let Some(condition) = object.get("if") else {
1278            return Err(unsupported_schema(
1279                source_path,
1280                "Schema `then` and `else` require an `if` Schema",
1281            ));
1282        };
1283        validate_schema_profile(condition, source_path)?;
1284    } else if let Some(condition) = object.get("if") {
1285        validate_schema_profile(condition, source_path)?;
1286    }
1287    for keyword in ["then", "else"] {
1288        if let Some(branch) = object.get(keyword) {
1289            validate_schema_profile(branch, source_path)?;
1290        }
1291    }
1292
1293    Ok(())
1294}
1295
1296fn validate_schema_type(schema_type: &str, source_path: &Path) -> Result<(), CodegenError> {
1297    if PORTABLE_SCHEMA_TYPES.contains(&schema_type) {
1298        Ok(())
1299    } else {
1300        Err(unsupported_schema(
1301            source_path,
1302            format!("Schema type `{schema_type}` is outside the portable profile"),
1303        ))
1304    }
1305}
1306
1307/// Compiles only the regular-expression syntax whose match semantics are shared
1308/// by Rust `regex` and ECMAScript Unicode-mode `RegExp` for portable Schemas.
1309/// Engine-specific shorthand classes, internal anchors, wildcard dots,
1310/// lookarounds, backreferences, inline flags, and backtracking-unsafe
1311/// repetition shapes fail closed before either runtime uses them. Fully
1312/// anchored patterns may use the complete safe subset; start-anchored prefix
1313/// patterns are limited to fixed-shape expressions.
1314fn compile_portable_pattern(pattern: &str) -> Result<Regex, String> {
1315    if !has_portable_pattern_syntax(pattern) {
1316        return Err("pattern uses syntax outside the shared Rust/ECMAScript subset".to_owned());
1317    }
1318    Regex::new(pattern).map_err(|error| error.to_string())
1319}
1320
1321#[derive(Clone, Copy, Debug, Default)]
1322#[expect(
1323    clippy::struct_excessive_bools,
1324    reason = "these are independent regex risk facts mirrored explicitly in the TypeScript gate"
1325)]
1326struct PortablePatternAtom {
1327    can_match_empty: bool,
1328    has_variable_shape: bool,
1329    is_quantified: bool,
1330    prefix_can_match_empty: bool,
1331    contains_variable_repetition: bool,
1332    contains_alternation: bool,
1333    is_group: bool,
1334}
1335
1336#[derive(Debug)]
1337#[expect(
1338    clippy::struct_excessive_bools,
1339    reason = "branch-local and group-wide regex risk facts are independent parser state"
1340)]
1341struct PortablePatternFrame {
1342    sequence_can_match_empty: bool,
1343    earlier_branch_can_match_empty: bool,
1344    contains_variable_repetition: bool,
1345    contains_alternation: bool,
1346    branch_has_variable_shape: bool,
1347    last_atom: Option<PortablePatternAtom>,
1348}
1349
1350impl Default for PortablePatternFrame {
1351    fn default() -> Self {
1352        Self {
1353            sequence_can_match_empty: true,
1354            earlier_branch_can_match_empty: false,
1355            contains_variable_repetition: false,
1356            contains_alternation: false,
1357            branch_has_variable_shape: false,
1358            last_atom: None,
1359        }
1360    }
1361}
1362
1363#[derive(Clone, Copy, Debug)]
1364struct PortableRepetition {
1365    minimum: u32,
1366    maximum: Option<u32>,
1367}
1368
1369impl PortableRepetition {
1370    fn has_variable_extent(self) -> bool {
1371        self.maximum != Some(self.minimum)
1372    }
1373}
1374
1375#[expect(
1376    clippy::too_many_lines,
1377    reason = "keeping the single-pass scanner locally isomorphic with the TypeScript gate makes parity auditable"
1378)]
1379fn has_portable_pattern_syntax(pattern: &str) -> bool {
1380    if pattern.chars().count() > MAX_PORTABLE_PATTERN_CHARS
1381        || pattern.as_bytes().first() != Some(&b'^')
1382    {
1383        return false;
1384    }
1385
1386    let bytes = pattern.as_bytes();
1387    let mut index = 0;
1388    let mut in_class = false;
1389    let mut class_start = None;
1390    let mut frames = vec![PortablePatternFrame::default()];
1391    while index < bytes.len() {
1392        if in_class {
1393            match bytes[index] {
1394                b'\\' => {
1395                    if pattern[index..].starts_with(PORTABLE_UNICODE_PROPERTY_ESCAPE) {
1396                        index += PORTABLE_UNICODE_PROPERTY_ESCAPE.len();
1397                        continue;
1398                    }
1399                    let Some(&escaped) = bytes.get(index + 1) else {
1400                        return false;
1401                    };
1402                    if !matches!(
1403                        escaped,
1404                        b'^' | b'$'
1405                            | b'\\'
1406                            | b'.'
1407                            | b'*'
1408                            | b'+'
1409                            | b'?'
1410                            | b'('
1411                            | b')'
1412                            | b'['
1413                            | b']'
1414                            | b'{'
1415                            | b'}'
1416                            | b'|'
1417                            | b'/'
1418                            | b'-'
1419                    ) {
1420                        return false;
1421                    }
1422                    index += 2;
1423                }
1424                b']' => {
1425                    let Some(start) = class_start.take() else {
1426                        return false;
1427                    };
1428                    if matches!(&pattern[start..index], "" | "^") {
1429                        return false;
1430                    }
1431                    in_class = false;
1432                    let Some(frame) = frames.last_mut() else {
1433                        return false;
1434                    };
1435                    if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1436                        return false;
1437                    }
1438                    index += 1;
1439                }
1440                b'[' => return false,
1441                operator @ (b'&' | b'-' | b'~' | b'|')
1442                    if bytes.get(index + 1) == Some(&operator) =>
1443                {
1444                    return false;
1445                }
1446                byte if byte.is_ascii_control() => return false,
1447                byte if byte.is_ascii() => index += 1,
1448                _ => {
1449                    let Some(character) = pattern[index..].chars().next() else {
1450                        return false;
1451                    };
1452                    if character.is_control() {
1453                        return false;
1454                    }
1455                    index += character.len_utf8();
1456                }
1457            }
1458            continue;
1459        }
1460
1461        match bytes[index] {
1462            b'\\' => {
1463                if pattern[index..].starts_with(PORTABLE_UNICODE_PROPERTY_ESCAPE) {
1464                    index += PORTABLE_UNICODE_PROPERTY_ESCAPE.len();
1465                } else {
1466                    let Some(&escaped) = bytes.get(index + 1) else {
1467                        return false;
1468                    };
1469                    if !matches!(
1470                        escaped,
1471                        b'^' | b'$'
1472                            | b'\\'
1473                            | b'.'
1474                            | b'*'
1475                            | b'+'
1476                            | b'?'
1477                            | b'('
1478                            | b')'
1479                            | b'['
1480                            | b']'
1481                            | b'{'
1482                            | b'}'
1483                            | b'|'
1484                            | b'/'
1485                    ) {
1486                        return false;
1487                    }
1488                    index += 2;
1489                }
1490                let Some(frame) = frames.last_mut() else {
1491                    return false;
1492                };
1493                if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1494                    return false;
1495                }
1496            }
1497            b'[' => {
1498                in_class = true;
1499                class_start = Some(index + 1);
1500                index += 1;
1501            }
1502            b'(' => {
1503                if bytes.get(index + 1) == Some(&b'?') {
1504                    if !pattern[index..].starts_with("(?:") {
1505                        return false;
1506                    }
1507                    index += 3;
1508                } else {
1509                    index += 1;
1510                }
1511                frames.push(PortablePatternFrame::default());
1512            }
1513            b')' => {
1514                if frames.len() == 1 {
1515                    return false;
1516                }
1517                let Some(group) = frames.pop() else {
1518                    return false;
1519                };
1520                let can_match_empty =
1521                    group.earlier_branch_can_match_empty || group.sequence_can_match_empty;
1522                if can_match_empty {
1523                    return false;
1524                }
1525                let atom = PortablePatternAtom {
1526                    can_match_empty,
1527                    has_variable_shape: group.contains_variable_repetition
1528                        || group.contains_alternation,
1529                    contains_variable_repetition: group.contains_variable_repetition,
1530                    contains_alternation: group.contains_alternation,
1531                    is_group: true,
1532                    ..PortablePatternAtom::default()
1533                };
1534                let Some(parent) = frames.last_mut() else {
1535                    return false;
1536                };
1537                if !push_portable_pattern_atom(parent, atom) {
1538                    return false;
1539                }
1540                index += 1;
1541            }
1542            b'|' => {
1543                if frames.len() == 1 {
1544                    return false;
1545                }
1546                let Some(frame) = frames.last_mut() else {
1547                    return false;
1548                };
1549                frame.contains_alternation = true;
1550                frame.earlier_branch_can_match_empty |= frame.sequence_can_match_empty;
1551                frame.sequence_can_match_empty = true;
1552                frame.branch_has_variable_shape = false;
1553                frame.last_atom = None;
1554                index += 1;
1555            }
1556            b'*' | b'+' | b'?' => {
1557                let repetition = match bytes[index] {
1558                    b'*' => PortableRepetition {
1559                        minimum: 0,
1560                        maximum: None,
1561                    },
1562                    b'+' => PortableRepetition {
1563                        minimum: 1,
1564                        maximum: None,
1565                    },
1566                    b'?' => PortableRepetition {
1567                        minimum: 0,
1568                        maximum: Some(1),
1569                    },
1570                    _ => unreachable!(),
1571                };
1572                let Some(frame) = frames.last_mut() else {
1573                    return false;
1574                };
1575                if !apply_portable_repetition(frame, repetition) {
1576                    return false;
1577                }
1578                index += 1;
1579            }
1580            b'{' => {
1581                let Some(close_offset) = pattern[index + 1..].find('}') else {
1582                    return false;
1583                };
1584                let close = index + 1 + close_offset;
1585                let Some(repetition) = parse_portable_repetition(&pattern[index + 1..close]) else {
1586                    return false;
1587                };
1588                let Some(frame) = frames.last_mut() else {
1589                    return false;
1590                };
1591                if !apply_portable_repetition(frame, repetition) {
1592                    return false;
1593                }
1594                index = close + 1;
1595            }
1596            b'^' if index == 0 => {
1597                let Some(frame) = frames.last_mut() else {
1598                    return false;
1599                };
1600                frame.last_atom = None;
1601                index += 1;
1602            }
1603            b'$' if is_portable_end_anchor(pattern, index) => {
1604                let Some(frame) = frames.last_mut() else {
1605                    return false;
1606                };
1607                frame.last_atom = None;
1608                index += 1;
1609            }
1610            b']' | b'}' | b'.' | b'^' | b'$' => return false,
1611            byte if byte.is_ascii_control() => return false,
1612            byte if byte.is_ascii() => {
1613                let Some(frame) = frames.last_mut() else {
1614                    return false;
1615                };
1616                if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1617                    return false;
1618                }
1619                index += 1;
1620            }
1621            _ => {
1622                let Some(character) = pattern[index..].chars().next() else {
1623                    return false;
1624                };
1625                if character.is_control() {
1626                    return false;
1627                }
1628                let Some(frame) = frames.last_mut() else {
1629                    return false;
1630                };
1631                if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1632                    return false;
1633                }
1634                index += character.len_utf8();
1635            }
1636        }
1637    }
1638
1639    if in_class || frames.len() != 1 {
1640        return false;
1641    }
1642    let frame = &frames[0];
1643    has_portable_end_anchor(pattern)
1644        || (frame.last_atom.is_some()
1645            && !frame.contains_variable_repetition
1646            && !frame.contains_alternation)
1647}
1648
1649fn has_portable_end_anchor(pattern: &str) -> bool {
1650    let bytes = pattern.as_bytes();
1651    if bytes.last() != Some(&b'$') {
1652        return false;
1653    }
1654    bytes[..bytes.len() - 1]
1655        .iter()
1656        .rev()
1657        .take_while(|byte| **byte == b'\\')
1658        .count()
1659        % 2
1660        == 0
1661}
1662
1663fn is_portable_end_anchor(pattern: &str, index: usize) -> bool {
1664    index + 1 == pattern.len() && has_portable_end_anchor(pattern)
1665}
1666
1667fn push_portable_pattern_atom(
1668    frame: &mut PortablePatternFrame,
1669    mut atom: PortablePatternAtom,
1670) -> bool {
1671    if atom.has_variable_shape && frame.branch_has_variable_shape {
1672        return false;
1673    }
1674    atom.prefix_can_match_empty = frame.sequence_can_match_empty;
1675    frame.sequence_can_match_empty &= atom.can_match_empty;
1676    frame.branch_has_variable_shape |= atom.has_variable_shape;
1677    frame.contains_variable_repetition |= atom.contains_variable_repetition;
1678    frame.contains_alternation |= atom.contains_alternation;
1679    frame.last_atom = Some(atom);
1680    true
1681}
1682
1683fn apply_portable_repetition(
1684    frame: &mut PortablePatternFrame,
1685    repetition: PortableRepetition,
1686) -> bool {
1687    let Some(atom) = frame.last_atom.as_mut() else {
1688        return false;
1689    };
1690    if atom.is_quantified || repetition.maximum == Some(0) {
1691        return false;
1692    }
1693    if atom.is_group
1694        && (atom.can_match_empty || atom.contains_variable_repetition || atom.contains_alternation)
1695    {
1696        return false;
1697    }
1698
1699    let has_variable_extent = repetition.has_variable_extent();
1700    if has_variable_extent && frame.branch_has_variable_shape {
1701        return false;
1702    }
1703    atom.is_quantified = true;
1704    atom.has_variable_shape |= has_variable_extent;
1705    atom.can_match_empty = repetition.minimum == 0 || atom.can_match_empty;
1706    frame.sequence_can_match_empty = atom.prefix_can_match_empty && atom.can_match_empty;
1707    frame.branch_has_variable_shape |= has_variable_extent;
1708    frame.contains_variable_repetition |= has_variable_extent;
1709    true
1710}
1711
1712fn parse_portable_repetition(value: &str) -> Option<PortableRepetition> {
1713    let (minimum, maximum) = if let Some((minimum, maximum)) = value.split_once(',') {
1714        if maximum.contains(',') {
1715            return None;
1716        }
1717        let minimum = parse_portable_repetition_bound(minimum)?;
1718        let maximum = if maximum.is_empty() {
1719            None
1720        } else {
1721            Some(parse_portable_repetition_bound(maximum)?)
1722        };
1723        (minimum, maximum)
1724    } else {
1725        let minimum = parse_portable_repetition_bound(value)?;
1726        (minimum, Some(minimum))
1727    };
1728    if maximum.is_some_and(|maximum| maximum < minimum) {
1729        return None;
1730    }
1731    Some(PortableRepetition { minimum, maximum })
1732}
1733
1734fn parse_portable_repetition_bound(value: &str) -> Option<u32> {
1735    if value.is_empty()
1736        || value.len() > 1 && value.starts_with('0')
1737        || !value.bytes().all(|byte| byte.is_ascii_digit())
1738    {
1739        return None;
1740    }
1741    value
1742        .parse::<u32>()
1743        .ok()
1744        .filter(|value| *value <= MAX_PORTABLE_REPETITION)
1745}
1746
1747#[expect(
1748    clippy::cast_possible_truncation,
1749    clippy::cast_sign_loss,
1750    reason = "the finite, non-negative, integral, safe-integer checks make this conversion exact"
1751)]
1752fn non_negative_safe_schema_integer(value: &Value) -> Option<u64> {
1753    let number = value.as_f64()?;
1754    (number.is_finite()
1755        && number >= 0.0
1756        && number.fract() == 0.0
1757        && number <= 9_007_199_254_740_991.0)
1758        .then_some(number as u64)
1759}
1760
1761fn unsupported_schema(source_path: &Path, detail: impl Into<String>) -> CodegenError {
1762    CodegenError::UnsupportedSchema {
1763        path: source_path.to_path_buf(),
1764        detail: detail.into(),
1765    }
1766}
1767
1768#[allow(clippy::too_many_lines)]
1769fn validate_value_generation_schema(
1770    schema: &Value,
1771    source_path: &Path,
1772) -> Result<(), CodegenError> {
1773    let Some(object) = schema.as_object() else {
1774        return Err(CodegenError::UnsupportedSchema {
1775            path: source_path.to_path_buf(),
1776            detail: "generated value Schemas must be objects".to_owned(),
1777        });
1778    };
1779    if object.contains_key("oneOf") || object.contains_key("anyOf") {
1780        return Err(CodegenError::UnsupportedSchema {
1781            path: source_path.to_path_buf(),
1782            detail: "generated value Schemas do not support oneOf/anyOf unions".to_owned(),
1783        });
1784    }
1785    if object.contains_key("allOf") {
1786        return Err(CodegenError::UnsupportedSchema {
1787            path: source_path.to_path_buf(),
1788            detail: "generated value Schemas do not support allOf unions".to_owned(),
1789        });
1790    }
1791    if object.contains_key("const") {
1792        return Err(CodegenError::UnsupportedSchema {
1793            path: source_path.to_path_buf(),
1794            detail: "generated value Schemas do not support const fields".to_owned(),
1795        });
1796    }
1797    if let Some(types) = object.get("type").and_then(Value::as_array) {
1798        let non_null = types
1799            .iter()
1800            .filter(|schema_type| *schema_type != "null")
1801            .count();
1802        if non_null > 1 {
1803            return Err(CodegenError::UnsupportedSchema {
1804                path: source_path.to_path_buf(),
1805                detail: "generated value Schemas only support nullable type unions".to_owned(),
1806            });
1807        }
1808        let mut narrowed = object.clone();
1809        let Some(schema_type) = types.iter().find(|schema_type| *schema_type != "null") else {
1810            return Ok(());
1811        };
1812        narrowed.insert("type".to_owned(), schema_type.clone());
1813        return validate_value_generation_schema(&Value::Object(narrowed), source_path);
1814    }
1815    if let Some(values) = object.get("enum").and_then(Value::as_array) {
1816        if values.is_empty() || !values.iter().all(Value::is_string) {
1817            return Err(CodegenError::UnsupportedSchema {
1818                path: source_path.to_path_buf(),
1819                detail: "generated enum values must be non-empty strings".to_owned(),
1820            });
1821        }
1822        let mut enum_names = BTreeSet::new();
1823        for value in values.iter().filter_map(Value::as_str) {
1824            let name = pascal_case(value);
1825            if !is_generated_enum_variant(&name) || !enum_names.insert(name) {
1826                return Err(CodegenError::UnsupportedSchema {
1827                    path: source_path.to_path_buf(),
1828                    detail: "generated enum values collide after Rust variant generation"
1829                        .to_owned(),
1830                });
1831            }
1832        }
1833    }
1834    let Some(schema_type) = object.get("type").and_then(Value::as_str) else {
1835        if object.contains_key("enum") {
1836            return Ok(());
1837        }
1838        return Err(CodegenError::UnsupportedSchema {
1839            path: source_path.to_path_buf(),
1840            detail: "generated value Schemas need a supported type or string enum".to_owned(),
1841        });
1842    };
1843    match schema_type {
1844        "object" => {
1845            let properties = object
1846                .get("properties")
1847                .and_then(Value::as_object)
1848                .cloned()
1849                .unwrap_or_default();
1850            let mut rust_names = BTreeSet::new();
1851            let mut nested_type_names = BTreeSet::new();
1852            for (name, property) in &properties {
1853                if !rust_names.insert(rust_field_name(name)) {
1854                    return Err(CodegenError::UnsupportedSchema {
1855                        path: source_path.to_path_buf(),
1856                        detail: format!(
1857                            "object properties collide after Rust field normalization near `{name}`"
1858                        ),
1859                    });
1860                }
1861                if !nested_type_names.insert(pascal_case(name)) {
1862                    return Err(CodegenError::UnsupportedSchema {
1863                        path: source_path.to_path_buf(),
1864                        detail: format!(
1865                            "object properties collide after nested type-name generation near `{name}`"
1866                        ),
1867                    });
1868                }
1869                validate_value_generation_schema(property, source_path)?;
1870            }
1871            match object.get("additionalProperties") {
1872                Some(Value::Bool(false)) => {}
1873                Some(Value::Bool(true)) | None if !properties.is_empty() => {
1874                    return Err(CodegenError::UnsupportedSchema {
1875                        path: source_path.to_path_buf(),
1876                        detail: "open objects with declared properties would lose unknown fields"
1877                            .to_owned(),
1878                    });
1879                }
1880                Some(additional) if !additional.is_boolean() && !properties.is_empty() => {
1881                    return Err(CodegenError::UnsupportedSchema {
1882                        path: source_path.to_path_buf(),
1883                        detail: "objects cannot combine declared properties with a map payload"
1884                            .to_owned(),
1885                    });
1886                }
1887                Some(additional) if !additional.is_boolean() => {
1888                    validate_value_generation_schema(additional, source_path)?;
1889                }
1890                _ => {}
1891            }
1892        }
1893        "array" => {
1894            let items = object
1895                .get("items")
1896                .ok_or_else(|| CodegenError::UnsupportedSchema {
1897                    path: source_path.to_path_buf(),
1898                    detail: "generated array Schemas must declare items".to_owned(),
1899                })?;
1900            validate_value_generation_schema(items, source_path)?;
1901        }
1902        "string" | "integer" | "number" | "boolean" | "null" => {}
1903        _ => {
1904            return Err(CodegenError::UnsupportedSchema {
1905                path: source_path.to_path_buf(),
1906                detail: format!("Schema type `{schema_type}` cannot be generated"),
1907            });
1908        }
1909    }
1910    Ok(())
1911}
1912
1913fn validate_domain_error_schema(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1914    if schema
1915        .as_object()
1916        .is_some_and(|object| object.contains_key("allOf"))
1917    {
1918        return Err(CodegenError::UnsupportedSchema {
1919            path: source_path.to_path_buf(),
1920            detail: "Domain Error Schemas do not support allOf unions".to_owned(),
1921        });
1922    }
1923    let variants = schema
1924        .get("oneOf")
1925        .and_then(Value::as_array)
1926        .filter(|variants| !variants.is_empty())
1927        .ok_or_else(|| CodegenError::UnsupportedSchema {
1928            path: source_path.to_path_buf(),
1929            detail: "Domain Error Schemas must be a non-empty oneOf union".to_owned(),
1930        })?;
1931    let mut codes = BTreeSet::new();
1932    let mut names = BTreeSet::from(["Unknown".to_owned()]);
1933    for variant in variants {
1934        if variant
1935            .as_object()
1936            .is_some_and(|object| object.contains_key("allOf"))
1937        {
1938            return Err(CodegenError::UnsupportedSchema {
1939                path: source_path.to_path_buf(),
1940                detail: "Domain Error variants do not support allOf unions".to_owned(),
1941            });
1942        }
1943        if let Some(code) = variant.get("const").and_then(Value::as_str) {
1944            let name = pascal_case(code);
1945            if !codes.insert(code.to_owned())
1946                || !names.insert(name.clone())
1947                || !is_generated_enum_variant(&name)
1948            {
1949                return Err(CodegenError::UnsupportedSchema {
1950                    path: source_path.to_path_buf(),
1951                    detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1952                });
1953            }
1954            continue;
1955        }
1956        let object = variant
1957            .as_object()
1958            .ok_or_else(|| CodegenError::UnsupportedSchema {
1959                path: source_path.to_path_buf(),
1960                detail: "Domain Error variants must be string consts or objects".to_owned(),
1961            })?;
1962        let properties = object
1963            .get("properties")
1964            .and_then(Value::as_object)
1965            .ok_or_else(|| CodegenError::UnsupportedSchema {
1966                path: source_path.to_path_buf(),
1967                detail: "structured Domain Error variants need properties".to_owned(),
1968            })?;
1969        if object.get("additionalProperties") != Some(&Value::Bool(false))
1970            || !required_fields(variant).contains("code")
1971            || properties
1972                .keys()
1973                .any(|name| name != "code" && name != "payload")
1974        {
1975            return Err(CodegenError::UnsupportedSchema {
1976                path: source_path.to_path_buf(),
1977                detail:
1978                    "structured Domain Error objects must explicitly allow only code and payload"
1979                        .to_owned(),
1980            });
1981        }
1982        let code = object
1983            .get("properties")
1984            .and_then(Value::as_object)
1985            .and_then(|properties| properties.get("code"))
1986            .and_then(|code| code.get("const"))
1987            .and_then(Value::as_str)
1988            .ok_or_else(|| CodegenError::UnsupportedSchema {
1989                path: source_path.to_path_buf(),
1990                detail: "structured Domain Error variants need a const string code".to_owned(),
1991            })?;
1992        let name = pascal_case(code);
1993        if !codes.insert(code.to_owned())
1994            || !names.insert(name.clone())
1995            || !is_generated_enum_variant(&name)
1996        {
1997            return Err(CodegenError::UnsupportedSchema {
1998                path: source_path.to_path_buf(),
1999                detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
2000            });
2001        }
2002        if let Some(payload) = object
2003            .get("properties")
2004            .and_then(Value::as_object)
2005            .and_then(|properties| properties.get("payload"))
2006        {
2007            validate_value_generation_schema(payload, source_path)?;
2008        }
2009    }
2010    Ok(())
2011}
2012
2013fn generation_input(path: &Path) -> Result<(GeneratedMetadata, ContractIr), CodegenError> {
2014    let descriptor = load_descriptor(path)?;
2015    let contract = contract_ir(&descriptor);
2016    let metadata = GeneratedMetadata {
2017        capability_id: descriptor.capability_id.clone(),
2018        descriptor_version: descriptor.version.clone(),
2019        descriptor_digest: descriptor.digest.clone(),
2020        portable: descriptor.portable,
2021        cross_lane_transfer: descriptor.cross_lane_transfer,
2022    };
2023    Ok((metadata, contract))
2024}
2025
2026/// Generates one selected language projection from a Descriptor source.
2027pub fn generate_projection(
2028    path: &Path,
2029    language: ProjectionLanguage,
2030) -> Result<GeneratedProjection, CodegenError> {
2031    let (metadata, contract) = generation_input(path)?;
2032    let source = match language {
2033        ProjectionLanguage::Rust => generate_rust(&contract),
2034        ProjectionLanguage::TypeScript => generate_typescript(&contract),
2035        ProjectionLanguage::Wit => wit::generate_wit(&contract)?,
2036        ProjectionLanguage::RustRuntime => generate_rust_runtime(&contract)?,
2037        ProjectionLanguage::RustPlugin => generate_rust_plugin(&contract)?,
2038    };
2039    Ok(GeneratedProjection {
2040        metadata,
2041        language,
2042        source,
2043    })
2044}
2045
2046/// Generates both language artifacts from one Descriptor source.
2047pub fn generate(path: &Path) -> Result<GeneratedArtifacts, CodegenError> {
2048    let (metadata, contract) = generation_input(path)?;
2049    Ok(GeneratedArtifacts {
2050        metadata,
2051        rust: generate_rust(&contract),
2052        typescript: generate_typescript(&contract),
2053    })
2054}
2055
2056/// Writes generated artifacts to checked-in paths.
2057pub fn write_generated(
2058    descriptor_path: &Path,
2059    rust_path: &Path,
2060    typescript_path: &Path,
2061) -> Result<(), CodegenError> {
2062    let artifacts = generate(descriptor_path)?;
2063    write_artifact(rust_path, &artifacts.rust)?;
2064    write_artifact(typescript_path, &artifacts.typescript)?;
2065    Ok(())
2066}
2067
2068/// Writes one selected language projection to a checked-in path.
2069pub fn write_projection(
2070    descriptor_path: &Path,
2071    language: ProjectionLanguage,
2072    output_path: &Path,
2073) -> Result<(), CodegenError> {
2074    let projection = generate_projection(descriptor_path, language)?;
2075    write_artifact(output_path, &projection.source)
2076}
2077
2078/// Fails when either checked-in artifact is not exactly reproducible.
2079pub fn check_generated(
2080    descriptor_path: &Path,
2081    rust_path: &Path,
2082    typescript_path: &Path,
2083) -> Result<(), CodegenError> {
2084    let artifacts = generate(descriptor_path)?;
2085    check_artifact(rust_path, &artifacts.rust)?;
2086    check_artifact(typescript_path, &artifacts.typescript)?;
2087    Ok(())
2088}
2089
2090/// Fails when one selected language projection is not exactly reproducible.
2091pub fn check_projection(
2092    descriptor_path: &Path,
2093    language: ProjectionLanguage,
2094    output_path: &Path,
2095) -> Result<(), CodegenError> {
2096    let projection = generate_projection(descriptor_path, language)?;
2097    check_artifact(output_path, &projection.source)
2098}
2099
2100/// Round-trips one JSON value through the portable wire representation.
2101///
2102/// Raw JSON numbers are restricted to JavaScript's safe integer range. Wide
2103/// integers must therefore be represented by the Descriptor's `int64` or
2104/// `uint64` decimal-string formats, which this function preserves exactly.
2105pub fn round_trip_portable_json(value: &Value) -> Result<Value, CodegenError> {
2106    validate_portable_value(value, "$".to_owned())?;
2107    let encoded = serde_json::to_vec(value).expect("JSON values are serializable");
2108    serde_json::from_slice(&encoded).map_err(|error| CodegenError::InvalidPortableValue {
2109        path: "$".to_owned(),
2110        detail: format!("wire JSON could not be decoded: {error}"),
2111    })
2112}
2113
2114/// Validates a wire value against a package-local JSON Schema.
2115///
2116/// This is intentionally a small validator for the Descriptor's portable
2117/// profile rather than a general JSON Schema implementation. It makes the
2118/// profile's loss-sensitive mappings executable: wide integers must use their
2119/// decimal-string formats, bytes must be canonical base64, and timestamp and
2120/// duration strings must have their documented wire shapes.
2121pub fn validate_wire_value(schema_path: &Path, value: &Value) -> Result<(), CodegenError> {
2122    let schema_path = canonical_path(schema_path)?;
2123    let package_root = schema_path
2124        .parent()
2125        .unwrap_or_else(|| Path::new("."))
2126        .to_path_buf();
2127    let source = read_json(&schema_path)?;
2128    let mut ref_stack = Vec::new();
2129    let schema = resolve_refs(
2130        &source,
2131        &source,
2132        &schema_path,
2133        &package_root,
2134        &mut ref_stack,
2135    )?;
2136    validate_portable_value(value, "$".to_owned())?;
2137    validate_schema_profile(&schema, &schema_path)?;
2138    validate_wire_value_inner(&schema, value, "$", &schema_path)
2139}
2140
2141fn validate_wire_value_inner(
2142    schema: &Value,
2143    value: &Value,
2144    path: &str,
2145    source_path: &Path,
2146) -> Result<(), CodegenError> {
2147    if let Some(constant) = schema.get("const")
2148        && !portable_json_semantic_equal(value, constant)
2149    {
2150        return invalid_wire_value(path, "value does not match the Schema const", source_path);
2151    }
2152    if let Some(values) = schema.get("enum").and_then(Value::as_array)
2153        && !values
2154            .iter()
2155            .any(|candidate| portable_json_semantic_equal(candidate, value))
2156    {
2157        return invalid_wire_value(
2158            path,
2159            "value is not included in the Schema enum",
2160            source_path,
2161        );
2162    }
2163
2164    if let Some(alternatives) = schema.get("oneOf").and_then(Value::as_array) {
2165        let matches = alternatives
2166            .iter()
2167            .filter(|alternative| {
2168                validate_wire_value_inner(alternative, value, path, source_path).is_ok()
2169            })
2170            .count();
2171        if matches == 0
2172            && is_open_domain_error_schema(schema)
2173            && is_unknown_domain_error_wire(schema, value)
2174        {
2175            return validate_portable_value(value, path.to_owned());
2176        }
2177        if matches != 1 {
2178            return invalid_wire_value(
2179                path,
2180                "value must match exactly one Schema oneOf alternative",
2181                source_path,
2182            );
2183        }
2184    }
2185    if let Some(alternatives) = schema.get("anyOf").and_then(Value::as_array)
2186        && !alternatives.iter().any(|alternative| {
2187            validate_wire_value_inner(alternative, value, path, source_path).is_ok()
2188        })
2189    {
2190        return invalid_wire_value(
2191            path,
2192            "value must match at least one Schema anyOf alternative",
2193            source_path,
2194        );
2195    }
2196    if let Some(condition) = schema.get("if") {
2197        let branch = if validate_wire_value_inner(condition, value, path, source_path).is_ok() {
2198            schema.get("then")
2199        } else {
2200            schema.get("else")
2201        };
2202        if let Some(branch) = branch {
2203            validate_wire_value_inner(branch, value, path, source_path)?;
2204        }
2205    }
2206
2207    match schema.get("type") {
2208        Some(Value::String(schema_type)) => {
2209            validate_wire_type(schema, schema_type, value, path, source_path)
2210        }
2211        Some(Value::Array(types)) => {
2212            let mut errors = Vec::new();
2213            for schema_type in types {
2214                let Some(schema_type) = schema_type.as_str() else {
2215                    continue;
2216                };
2217                match validate_wire_type(schema, schema_type, value, path, source_path) {
2218                    Ok(()) => return Ok(()),
2219                    Err(error) => errors.push(error.to_string()),
2220                }
2221            }
2222            invalid_wire_value(
2223                path,
2224                &format!(
2225                    "value does not match any Schema type ({})",
2226                    errors.join("; ")
2227                ),
2228                source_path,
2229            )
2230        }
2231        Some(_) => invalid_wire_value(path, "Schema type must be a string or array", source_path),
2232        None if schema.as_object().is_some_and(|object| {
2233            object.contains_key("properties")
2234                || object.contains_key("required")
2235                || object.contains_key("additionalProperties")
2236        }) =>
2237        {
2238            validate_wire_type(schema, "object", value, path, source_path)
2239        }
2240        None if schema
2241            .as_object()
2242            .is_some_and(|object| object.contains_key("const") || object.contains_key("enum")) =>
2243        {
2244            Ok(())
2245        }
2246        None => Ok(()),
2247    }
2248}
2249
2250fn is_open_domain_error_schema(schema: &Value) -> bool {
2251    schema
2252        .get("oneOf")
2253        .and_then(Value::as_array)
2254        .is_some_and(|variants| {
2255            !variants.is_empty()
2256                && variants.iter().all(|variant| {
2257                    variant.get("const").and_then(Value::as_str).is_some()
2258                        || variant
2259                            .get("properties")
2260                            .and_then(Value::as_object)
2261                            .and_then(|properties| properties.get("code"))
2262                            .and_then(|code| code.get("const"))
2263                            .and_then(Value::as_str)
2264                            .is_some()
2265                })
2266        })
2267}
2268
2269fn is_unknown_domain_error_wire(schema: &Value, value: &Value) -> bool {
2270    let known_codes = schema
2271        .get("oneOf")
2272        .and_then(Value::as_array)
2273        .into_iter()
2274        .flatten()
2275        .filter_map(|variant| {
2276            variant.get("const").and_then(Value::as_str).or_else(|| {
2277                variant
2278                    .get("properties")
2279                    .and_then(Value::as_object)
2280                    .and_then(|properties| properties.get("code"))
2281                    .and_then(|code| code.get("const"))
2282                    .and_then(Value::as_str)
2283            })
2284        })
2285        .collect::<BTreeSet<_>>();
2286    match value {
2287        Value::String(code) => !known_codes.contains(code.as_str()),
2288        Value::Object(object) => object
2289            .get("code")
2290            .and_then(Value::as_str)
2291            .is_some_and(|code| !known_codes.contains(code)),
2292        _ => false,
2293    }
2294}
2295
2296fn validate_wire_type(
2297    schema: &Value,
2298    schema_type: &str,
2299    value: &Value,
2300    path: &str,
2301    source_path: &Path,
2302) -> Result<(), CodegenError> {
2303    match schema_type {
2304        "object" => validate_wire_object(schema, value, path, source_path),
2305        "array" => validate_wire_array(schema, value, path, source_path),
2306        "string" => validate_wire_string(schema, value, path, source_path),
2307        "integer" => {
2308            if !value.is_number()
2309                || value.as_i64().is_none() && value.as_u64().is_none()
2310                || !is_safe_json_integer(value)
2311            {
2312                return invalid_wire_value(path, "expected an integer", source_path);
2313            }
2314            validate_numeric_constraint(schema, value, path, source_path)
2315        }
2316        "number" => {
2317            if value
2318                .as_number()
2319                .is_none_or(|number| !is_safe_json_number(number))
2320            {
2321                return invalid_wire_value(path, "expected a finite number", source_path);
2322            }
2323            validate_numeric_constraint(schema, value, path, source_path)
2324        }
2325        "boolean" => {
2326            if !value.is_boolean() {
2327                return invalid_wire_value(path, "expected a boolean", source_path);
2328            }
2329            Ok(())
2330        }
2331        "null" => {
2332            if !value.is_null() {
2333                return invalid_wire_value(path, "expected null", source_path);
2334            }
2335            Ok(())
2336        }
2337        _ => invalid_wire_value(
2338            path,
2339            "Schema type is outside the portable profile",
2340            source_path,
2341        ),
2342    }
2343}
2344
2345fn is_safe_json_integer(value: &Value) -> bool {
2346    value
2347        .as_i64()
2348        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
2349        || value
2350            .as_u64()
2351            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
2352}
2353
2354fn is_safe_json_number(number: &serde_json::Number) -> bool {
2355    number
2356        .as_i64()
2357        .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
2358        || number
2359            .as_u64()
2360            .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
2361        || number.as_f64().is_some_and(|value| {
2362            value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
2363        })
2364}
2365
2366fn validate_wire_object(
2367    schema: &Value,
2368    value: &Value,
2369    path: &str,
2370    source_path: &Path,
2371) -> Result<(), CodegenError> {
2372    let Value::Object(properties) = value else {
2373        return invalid_wire_value(path, "expected an object", source_path);
2374    };
2375    let property_count = u64::try_from(properties.len()).unwrap_or(u64::MAX);
2376    if let Some(minimum) = schema
2377        .get("minProperties")
2378        .and_then(non_negative_safe_schema_integer)
2379        && property_count < minimum
2380    {
2381        return invalid_wire_value(
2382            path,
2383            "object has fewer properties than minProperties",
2384            source_path,
2385        );
2386    }
2387    if let Some(maximum) = schema
2388        .get("maxProperties")
2389        .and_then(non_negative_safe_schema_integer)
2390        && property_count > maximum
2391    {
2392        return invalid_wire_value(
2393            path,
2394            "object has more properties than maxProperties",
2395            source_path,
2396        );
2397    }
2398    let required = required_fields(schema);
2399    for field in required {
2400        if !properties.contains_key(&field) {
2401            return invalid_wire_value(
2402                &format!("{path}.{field}"),
2403                "required field is missing",
2404                source_path,
2405            );
2406        }
2407    }
2408    let declared = schema
2409        .get("properties")
2410        .and_then(Value::as_object)
2411        .cloned()
2412        .unwrap_or_default();
2413    for (field, field_value) in properties {
2414        if let Some(property_names) = schema.get("propertyNames") {
2415            validate_wire_value_inner(
2416                property_names,
2417                &Value::String(field.clone()),
2418                &format!("{path}.{field}"),
2419                source_path,
2420            )?;
2421        }
2422        if let Some(field_schema) = declared.get(field) {
2423            validate_wire_value_inner(
2424                field_schema,
2425                field_value,
2426                &format!("{path}.{field}"),
2427                source_path,
2428            )?;
2429            continue;
2430        }
2431        match schema.get("additionalProperties") {
2432            Some(Value::Bool(false)) => {
2433                return invalid_wire_value(
2434                    &format!("{path}.{field}"),
2435                    "additional property is not allowed",
2436                    source_path,
2437                );
2438            }
2439            Some(additional_schema) if !additional_schema.is_boolean() => {
2440                validate_wire_value_inner(
2441                    additional_schema,
2442                    field_value,
2443                    &format!("{path}.{field}"),
2444                    source_path,
2445                )?;
2446            }
2447            _ => {}
2448        }
2449    }
2450    Ok(())
2451}
2452
2453fn validate_wire_array(
2454    schema: &Value,
2455    value: &Value,
2456    path: &str,
2457    source_path: &Path,
2458) -> Result<(), CodegenError> {
2459    let Value::Array(values) = value else {
2460        return invalid_wire_value(path, "expected an array", source_path);
2461    };
2462    let length = u64::try_from(values.len()).unwrap_or(u64::MAX);
2463    if let Some(minimum) = schema
2464        .get("minItems")
2465        .and_then(non_negative_safe_schema_integer)
2466        && length < minimum
2467    {
2468        return invalid_wire_value(path, "array has fewer items than minItems", source_path);
2469    }
2470    if let Some(maximum) = schema
2471        .get("maxItems")
2472        .and_then(non_negative_safe_schema_integer)
2473        && length > maximum
2474    {
2475        return invalid_wire_value(path, "array has more items than maxItems", source_path);
2476    }
2477    if schema.get("uniqueItems") == Some(&Value::Bool(true)) {
2478        let mut fingerprints = HashSet::with_capacity(values.len());
2479        for value in values {
2480            if !fingerprints.insert(portable_json_fingerprint(value)) {
2481                return invalid_wire_value(path, "array items must be unique", source_path);
2482            }
2483        }
2484    }
2485    if let Some(items) = schema.get("items") {
2486        for (index, item) in values.iter().enumerate() {
2487            validate_wire_value_inner(items, item, &format!("{path}[{index}]"), source_path)?;
2488        }
2489    }
2490    Ok(())
2491}
2492
2493fn portable_json_fingerprint(value: &Value) -> String {
2494    let mut output = String::new();
2495    write_portable_json_fingerprint(value, &mut output);
2496    output
2497}
2498
2499fn portable_json_semantic_equal(left: &Value, right: &Value) -> bool {
2500    portable_json_fingerprint(left) == portable_json_fingerprint(right)
2501}
2502
2503fn write_portable_json_fingerprint(value: &Value, output: &mut String) {
2504    match value {
2505        Value::Null => output.push('n'),
2506        Value::Bool(value) => output.push_str(if *value { "b1" } else { "b0" }),
2507        Value::Number(value) => {
2508            let number = value
2509                .as_f64()
2510                .expect("portable JSON numbers are representable as f64");
2511            let bits = if number == 0.0 { 0 } else { number.to_bits() };
2512            write!(output, "d{bits:016x};").expect("writing to a String cannot fail");
2513        }
2514        Value::String(value) => {
2515            write!(output, "s{}:", value.len()).expect("writing to a String cannot fail");
2516            output.push_str(value);
2517        }
2518        Value::Array(values) => {
2519            write!(output, "a{}[", values.len()).expect("writing to a String cannot fail");
2520            for value in values {
2521                write_portable_json_fingerprint(value, output);
2522            }
2523            output.push(']');
2524        }
2525        Value::Object(values) => {
2526            let mut entries = values.iter().collect::<Vec<_>>();
2527            entries.sort_unstable_by_key(|(key, _)| *key);
2528            write!(output, "o{}{{", entries.len()).expect("writing to a String cannot fail");
2529            for (key, value) in entries {
2530                write!(output, "k{}:", key.len()).expect("writing to a String cannot fail");
2531                output.push_str(key);
2532                write_portable_json_fingerprint(value, output);
2533            }
2534            output.push('}');
2535        }
2536    }
2537}
2538
2539fn validate_wire_string(
2540    schema: &Value,
2541    value: &Value,
2542    path: &str,
2543    source_path: &Path,
2544) -> Result<(), CodegenError> {
2545    let Value::String(value) = value else {
2546        return invalid_wire_value(path, "expected a string", source_path);
2547    };
2548    validate_string_format(schema, value, path, source_path)?;
2549    if let Some(pattern) = schema.get("pattern").and_then(Value::as_str) {
2550        let Ok(pattern) = compile_portable_pattern(pattern) else {
2551            return invalid_wire_value(path, "Schema pattern could not be compiled", source_path);
2552        };
2553        if !pattern.is_match(value) {
2554            return invalid_wire_value(path, "string does not match pattern", source_path);
2555        }
2556    }
2557    let length = u64::try_from(value.chars().count()).unwrap_or(u64::MAX);
2558    if let Some(minimum) = schema
2559        .get("minLength")
2560        .and_then(non_negative_safe_schema_integer)
2561        && length < minimum
2562    {
2563        return invalid_wire_value(path, "string is shorter than minLength", source_path);
2564    }
2565    if let Some(maximum) = schema
2566        .get("maxLength")
2567        .and_then(non_negative_safe_schema_integer)
2568        && length > maximum
2569    {
2570        return invalid_wire_value(path, "string is longer than maxLength", source_path);
2571    }
2572    Ok(())
2573}
2574
2575fn validate_numeric_constraint(
2576    schema: &Value,
2577    value: &Value,
2578    path: &str,
2579    source_path: &Path,
2580) -> Result<(), CodegenError> {
2581    let Some(number) = value.as_f64() else {
2582        return Ok(());
2583    };
2584    if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64)
2585        && number < minimum
2586    {
2587        return invalid_wire_value(path, "number is below minimum", source_path);
2588    }
2589    if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64)
2590        && number > maximum
2591    {
2592        return invalid_wire_value(path, "number is above maximum", source_path);
2593    }
2594    if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64)
2595        && number <= minimum
2596    {
2597        return invalid_wire_value(path, "number is not above exclusiveMinimum", source_path);
2598    }
2599    if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64)
2600        && number >= maximum
2601    {
2602        return invalid_wire_value(path, "number is not below exclusiveMaximum", source_path);
2603    }
2604    Ok(())
2605}
2606
2607fn validate_string_format(
2608    schema: &Value,
2609    value: &str,
2610    path: &str,
2611    source_path: &Path,
2612) -> Result<(), CodegenError> {
2613    let Some(format) = schema.get("format").and_then(Value::as_str) else {
2614        return Ok(());
2615    };
2616    let valid = match format {
2617        "int64" => is_signed_decimal(value),
2618        "uint64" => is_unsigned_decimal(value),
2619        "byte" => is_base64(value),
2620        "date-time" => is_rfc3339(value),
2621        "duration" => is_iso8601_duration(value),
2622        _ => false,
2623    };
2624    if valid {
2625        Ok(())
2626    } else {
2627        invalid_wire_value(
2628            path,
2629            &format!("string does not match portable `{format}` format"),
2630            source_path,
2631        )
2632    }
2633}
2634
2635fn invalid_wire_value<T>(path: &str, detail: &str, _source_path: &Path) -> Result<T, CodegenError> {
2636    Err(CodegenError::InvalidPortableValue {
2637        path: path.to_owned(),
2638        detail: detail.to_owned(),
2639    })
2640}
2641
2642fn is_signed_decimal(value: &str) -> bool {
2643    let digits = value.strip_prefix('-').unwrap_or(value);
2644    !digits.is_empty()
2645        && (digits == "0" || !digits.starts_with('0'))
2646        && digits.chars().all(|character| character.is_ascii_digit())
2647        && value.parse::<i64>().is_ok()
2648}
2649
2650fn is_unsigned_decimal(value: &str) -> bool {
2651    !value.is_empty()
2652        && (value == "0" || !value.starts_with('0'))
2653        && value.chars().all(|character| character.is_ascii_digit())
2654        && value.parse::<u64>().is_ok()
2655}
2656
2657fn is_base64(value: &str) -> bool {
2658    let bytes = value.as_bytes();
2659    if bytes.is_empty() {
2660        return true;
2661    }
2662    if !bytes.len().is_multiple_of(4) {
2663        return false;
2664    }
2665    let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count();
2666    if padding > 2 || bytes[..bytes.len() - padding].contains(&b'=') {
2667        return false;
2668    }
2669    for byte in &bytes[..bytes.len() - padding] {
2670        if base64_digit(*byte).is_none() {
2671            return false;
2672        }
2673    }
2674    let last = &bytes[bytes.len() - 4..];
2675    if padding == 1 {
2676        base64_digit(last[2]).is_some_and(|digit| digit.trailing_zeros() >= 2)
2677    } else if padding == 2 {
2678        base64_digit(last[1]).is_some_and(|digit| digit.trailing_zeros() >= 4)
2679    } else {
2680        base64_digit(last[2]).is_some() && base64_digit(last[3]).is_some()
2681    }
2682}
2683
2684fn base64_digit(byte: u8) -> Option<u8> {
2685    match byte {
2686        b'A'..=b'Z' => Some(byte - b'A'),
2687        b'a'..=b'z' => Some(byte - b'a' + 26),
2688        b'0'..=b'9' => Some(byte - b'0' + 52),
2689        b'+' => Some(62),
2690        b'/' => Some(63),
2691        _ => None,
2692    }
2693}
2694
2695fn is_rfc3339(value: &str) -> bool {
2696    let bytes = value.as_bytes();
2697    if bytes.len() < 20
2698        || !matches!(bytes.get(4), Some(b'-'))
2699        || !matches!(bytes.get(7), Some(b'-'))
2700        || !matches!(bytes.get(10), Some(b'T' | b't'))
2701        || !matches!(bytes.get(13), Some(b':'))
2702        || !matches!(bytes.get(16), Some(b':'))
2703    {
2704        return false;
2705    }
2706    let Some(month) = fixed_digits(bytes, 5, 2) else {
2707        return false;
2708    };
2709    let Some(day) = fixed_digits(bytes, 8, 2) else {
2710        return false;
2711    };
2712    let Some(hour) = fixed_digits(bytes, 11, 2) else {
2713        return false;
2714    };
2715    let Some(minute) = fixed_digits(bytes, 14, 2) else {
2716        return false;
2717    };
2718    let Some(second) = fixed_digits(bytes, 17, 2) else {
2719        return false;
2720    };
2721    let Some(year) = fixed_digits(bytes, 0, 4) else {
2722        return false;
2723    };
2724    let max_day = match month {
2725        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2726        4 | 6 | 9 | 11 => 30,
2727        2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
2728        2 => 28,
2729        _ => return false,
2730    };
2731    if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
2732        return false;
2733    }
2734    let mut index = 19;
2735    if bytes.get(index) == Some(&b'.') {
2736        index += 1;
2737        let start = index;
2738        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2739            index += 1;
2740        }
2741        if index == start {
2742            return false;
2743        }
2744    }
2745    match bytes.get(index) {
2746        Some(b'Z' | b'z') => index + 1 == bytes.len(),
2747        Some(b'+' | b'-') => {
2748            index += 1;
2749            let Some(offset_hour) = fixed_digits(bytes, index, 2) else {
2750                return false;
2751            };
2752            index += 2;
2753            if bytes.get(index) != Some(&b':') {
2754                return false;
2755            }
2756            index += 1;
2757            let Some(offset_minute) = fixed_digits(bytes, index, 2) else {
2758                return false;
2759            };
2760            offset_hour <= 23 && offset_minute <= 59 && index + 2 == bytes.len()
2761        }
2762        _ => false,
2763    }
2764}
2765
2766fn fixed_digits(bytes: &[u8], start: usize, length: usize) -> Option<u32> {
2767    let slice = bytes.get(start..start + length)?;
2768    slice.iter().try_fold(0_u32, |value, digit| {
2769        digit
2770            .is_ascii_digit()
2771            .then(|| value * 10 + u32::from(digit - b'0'))
2772    })
2773}
2774
2775fn is_iso8601_duration(value: &str) -> bool {
2776    let bytes = value.as_bytes();
2777    let mut index = usize::from(bytes.first() == Some(&b'-'));
2778    if bytes.get(index) != Some(&b'P') {
2779        return false;
2780    }
2781    index += 1;
2782    let mut in_time = false;
2783    let mut saw_component = false;
2784    let mut saw_time_component = false;
2785    let mut last_date_order = None;
2786    let mut last_time_order = None;
2787    let mut saw_week = false;
2788    let mut saw_other_date_unit = false;
2789    while index < bytes.len() {
2790        if bytes[index] == b'T' {
2791            if in_time || index + 1 == bytes.len() {
2792                return false;
2793            }
2794            in_time = true;
2795            index += 1;
2796            continue;
2797        }
2798        let digit_start = index;
2799        while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2800            index += 1;
2801        }
2802        if index == digit_start {
2803            return false;
2804        }
2805
2806        let mut fractional = false;
2807        if bytes.get(index) == Some(&b'.') {
2808            fractional = true;
2809            index += 1;
2810            let fraction_start = index;
2811            while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2812                index += 1;
2813            }
2814            if index == fraction_start {
2815                return false;
2816            }
2817        }
2818
2819        let Some(&unit) = bytes.get(index) else {
2820            return false;
2821        };
2822        if in_time {
2823            let order = match unit {
2824                b'H' => 0,
2825                b'M' => 1,
2826                b'S' => 2,
2827                _ => return false,
2828            };
2829            if last_time_order.is_some_and(|previous| order <= previous) {
2830                return false;
2831            }
2832            last_time_order = Some(order);
2833            saw_time_component = true;
2834        } else {
2835            let order = match unit {
2836                b'Y' => 0,
2837                b'M' => 1,
2838                b'W' => 2,
2839                b'D' => 3,
2840                _ => return false,
2841            };
2842            if last_date_order.is_some_and(|previous| order <= previous) {
2843                return false;
2844            }
2845            last_date_order = Some(order);
2846            if unit == b'W' {
2847                if saw_other_date_unit || index + 1 != bytes.len() {
2848                    return false;
2849                }
2850                saw_week = true;
2851            } else {
2852                if saw_week {
2853                    return false;
2854                }
2855                saw_other_date_unit = true;
2856            }
2857        }
2858        saw_component = true;
2859        index += 1;
2860        if fractional && index != bytes.len() {
2861            return false;
2862        }
2863    }
2864    saw_component && (!in_time || saw_time_component)
2865}
2866
2867fn validate_portable_value(value: &Value, path: String) -> Result<(), CodegenError> {
2868    match value {
2869        Value::Number(number) => {
2870            if !is_safe_json_number(number) {
2871                return Err(CodegenError::InvalidPortableValue {
2872                    path,
2873                    detail: "wide integers must use an explicit decimal-string format".to_owned(),
2874                });
2875            }
2876        }
2877        Value::Array(values) => {
2878            for (index, value) in values.iter().enumerate() {
2879                validate_portable_value(value, format!("{path}[{index}]"))?;
2880            }
2881        }
2882        Value::Object(values) => {
2883            for (key, value) in values {
2884                validate_portable_value(value, format!("{path}.{key}"))?;
2885            }
2886        }
2887        Value::Null | Value::Bool(_) | Value::String(_) => {}
2888    }
2889    Ok(())
2890}
2891
2892fn write_artifact(path: &Path, contents: &str) -> Result<(), CodegenError> {
2893    if let Some(parent) = path.parent() {
2894        fs::create_dir_all(parent).map_err(|source| CodegenError::Io {
2895            path: parent.to_path_buf(),
2896            source,
2897        })?;
2898    }
2899    fs::write(path, contents).map_err(|source| CodegenError::Io {
2900        path: path.to_path_buf(),
2901        source,
2902    })
2903}
2904
2905fn check_artifact(path: &Path, expected: &str) -> Result<(), CodegenError> {
2906    let actual = fs::read_to_string(path).map_err(|source| CodegenError::Io {
2907        path: path.to_path_buf(),
2908        source,
2909    })?;
2910    if actual == expected {
2911        Ok(())
2912    } else {
2913        Err(CodegenError::GeneratedArtifactDrift {
2914            path: path.to_path_buf(),
2915        })
2916    }
2917}
2918
2919/// Checks whether `new_path` is an additive, compatible evolution of
2920/// `old_path`. `true` means the same Capability series remains compatible.
2921pub fn lint_compatibility(old_path: &Path, new_path: &Path) -> Result<bool, CompatibilityError> {
2922    let old = load_descriptor(old_path).map_err(|error| CompatibilityError::BreakingChanges {
2923        changes: vec![error.to_string()],
2924    })?;
2925    let new = load_descriptor(new_path).map_err(|error| CompatibilityError::BreakingChanges {
2926        changes: vec![error.to_string()],
2927    })?;
2928    if old.capability_id != new.capability_id {
2929        return Err(CompatibilityError::IdentityChanged {
2930            from: old.capability_id,
2931            to: new.capability_id,
2932        });
2933    }
2934    if new.parsed_version <= old.parsed_version {
2935        return Err(CompatibilityError::VersionNotAdvanced {
2936            from: old.version,
2937            to: new.version,
2938        });
2939    }
2940    if new.parsed_version.major() != old.parsed_version.major() {
2941        return Err(CompatibilityError::BreakingChanges {
2942            changes: vec!["a breaking change must use a new Capability @major identity".to_owned()],
2943        });
2944    }
2945
2946    let mut changes = Vec::new();
2947    if old.portable != new.portable {
2948        changes.push("Descriptor portability changed".to_owned());
2949    }
2950    if old.cross_lane_transfer && !new.cross_lane_transfer {
2951        changes.push("Descriptor cross-lane transfer support was removed".to_owned());
2952    }
2953    let old_operations: BTreeMap<_, _> = old
2954        .operations
2955        .iter()
2956        .map(|operation| (operation.name.as_str(), operation))
2957        .collect();
2958    let new_operations: BTreeMap<_, _> = new
2959        .operations
2960        .iter()
2961        .map(|operation| (operation.name.as_str(), operation))
2962        .collect();
2963
2964    for (name, old_operation) in &old_operations {
2965        let Some(new_operation) = new_operations.get(name) else {
2966            changes.push(format!("Operation `{name}` was removed"));
2967            continue;
2968        };
2969        if old_operation.interaction != new_operation.interaction {
2970            changes.push(format!("Operation `{name}` changed interaction"));
2971        }
2972        compare_schema(
2973            &old_operation.request_schema,
2974            &new_operation.request_schema,
2975            &format!("Operation `{name}` request"),
2976            &mut changes,
2977        );
2978        compare_schema(
2979            &old_operation.response_schema,
2980            &new_operation.response_schema,
2981            &format!("Operation `{name}` response"),
2982            &mut changes,
2983        );
2984        compare_error_schema(
2985            &old_operation.domain_error_schema,
2986            &new_operation.domain_error_schema,
2987            &format!("Operation `{name}` Domain Error"),
2988            &mut changes,
2989        );
2990    }
2991
2992    if new.parsed_version.minor() == old.parsed_version.minor() {
2993        if contract_signature(&old) != contract_signature(&new) {
2994            changes.push("a patch release changed the observable contract".to_owned());
2995        }
2996        for name in new_operations
2997            .keys()
2998            .filter(|name| !old_operations.contains_key(**name))
2999        {
3000            changes.push(format!("Operation `{name}` was added in a patch release"));
3001        }
3002    }
3003    if new.parsed_version.minor() < old.parsed_version.minor() {
3004        changes.push("Descriptor minor version moved backwards".to_owned());
3005    }
3006
3007    if changes.is_empty() {
3008        Ok(true)
3009    } else {
3010        Err(CompatibilityError::BreakingChanges { changes })
3011    }
3012}
3013
3014fn contract_signature(descriptor: &Descriptor) -> String {
3015    let operations = descriptor
3016        .operations
3017        .iter()
3018        .map(|operation| {
3019            serde_json::json!({
3020                "name": operation.name,
3021                "interaction": operation.interaction,
3022                "request": operation.request_schema,
3023                "response": operation.response_schema,
3024                "domain_error": canonical_domain_error_schema(&operation.domain_error_schema),
3025            })
3026        })
3027        .collect::<Vec<_>>();
3028    canonical_json(&Value::Array(operations))
3029}
3030
3031fn canonical_domain_error_schema(schema: &Value) -> Value {
3032    let Some(variants) = schema.get("oneOf").and_then(Value::as_array) else {
3033        return schema.clone();
3034    };
3035    let mut variants = variants.clone();
3036    variants.sort_by(|left, right| {
3037        error_variant_code(left)
3038            .unwrap_or_default()
3039            .cmp(error_variant_code(right).unwrap_or_default())
3040    });
3041    let mut schema = schema.as_object().cloned().unwrap_or_default();
3042    schema.insert("oneOf".to_owned(), Value::Array(variants));
3043    Value::Object(schema)
3044}
3045
3046fn error_variant_code(variant: &Value) -> Option<&str> {
3047    variant.get("const").and_then(Value::as_str).or_else(|| {
3048        variant
3049            .get("properties")
3050            .and_then(Value::as_object)
3051            .and_then(|properties| properties.get("code"))
3052            .and_then(|code| code.get("const"))
3053            .and_then(Value::as_str)
3054    })
3055}
3056
3057fn compare_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
3058    let old_nullable = is_nullable(old);
3059    let new_nullable = is_nullable(new);
3060    if old_nullable != new_nullable {
3061        changes.push(format!("{location} changed nullability"));
3062        return;
3063    }
3064    let old_type = schema_type_name(old);
3065    let new_type = schema_type_name(new);
3066    if old_type != new_type {
3067        changes.push(format!("{location} changed type"));
3068        return;
3069    }
3070    match old_type {
3071        Some("object") => {
3072            let old_properties = old.get("properties").and_then(Value::as_object);
3073            let new_properties = new.get("properties").and_then(Value::as_object);
3074            let (Some(old_properties), Some(new_properties)) = (old_properties, new_properties)
3075            else {
3076                if canonical_json(old) != canonical_json(new) {
3077                    changes.push(format!("{location} changed object shape"));
3078                }
3079                return;
3080            };
3081            let old_required = required_fields(old);
3082            let new_required = required_fields(new);
3083            for name in old_properties.keys() {
3084                let Some(new_schema) = new_properties.get(name) else {
3085                    changes.push(format!("{location} field `{name}` was removed"));
3086                    continue;
3087                };
3088                compare_schema(
3089                    &old_properties[name],
3090                    new_schema,
3091                    &format!("{location} field `{name}`"),
3092                    changes,
3093                );
3094            }
3095            for name in new_required.difference(&old_required) {
3096                changes.push(format!("{location} field `{name}` became required"));
3097            }
3098            for name in old_required.difference(&new_required) {
3099                changes.push(format!("{location} field `{name}` became optional"));
3100            }
3101            if old.get("additionalProperties") != new.get("additionalProperties") {
3102                changes.push(format!("{location} changed additional-properties policy"));
3103            }
3104            if schema_constraints_signature(old) != schema_constraints_signature(new) {
3105                changes.push(format!("{location} changed constraints"));
3106            }
3107        }
3108        Some("array") => {
3109            if let (Some(old_items), Some(new_items)) = (old.get("items"), new.get("items")) {
3110                compare_schema(old_items, new_items, &format!("{location} items"), changes);
3111            }
3112            if schema_constraints_signature(old) != schema_constraints_signature(new) {
3113                changes.push(format!("{location} changed constraints"));
3114            }
3115        }
3116        _ => {
3117            if canonical_json(old) != canonical_json(new) {
3118                changes.push(format!("{location} changed format or constraints"));
3119            }
3120        }
3121    }
3122}
3123
3124fn compare_error_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
3125    let old_variants = error_variant_values(old);
3126    let new_variants = error_variant_values(new);
3127    if old_variants.is_empty() || new_variants.is_empty() {
3128        if canonical_json(old) != canonical_json(new) {
3129            changes.push(format!("{location} changed shape"));
3130        }
3131        return;
3132    }
3133    for (code, schema) in old_variants {
3134        match new_variants.get(&code) {
3135            None => changes.push(format!("{location} variant `{code}` was removed")),
3136            Some(new_schema) => compare_schema(
3137                &schema,
3138                new_schema,
3139                &format!("{location} variant `{code}`"),
3140                changes,
3141            ),
3142        }
3143    }
3144}
3145
3146fn error_variant_ir_definitions(schema: &Value) -> Vec<ErrorVariantIr> {
3147    let mut variants = schema
3148        .get("oneOf")
3149        .and_then(Value::as_array)
3150        .into_iter()
3151        .flatten()
3152        .filter_map(|variant| {
3153            if let Some(code) = variant.get("const").and_then(Value::as_str) {
3154                return Some(ErrorVariantIr {
3155                    code: code.to_owned(),
3156                    name: pascal_case(code),
3157                    structured: false,
3158                    payload: None,
3159                    payload_required: false,
3160                });
3161            }
3162            let object = variant.as_object()?;
3163            let code = object
3164                .get("properties")
3165                .and_then(Value::as_object)
3166                .and_then(|properties| properties.get("code"))
3167                .and_then(|code| code.get("const"))
3168                .and_then(Value::as_str)?;
3169            Some(ErrorVariantIr {
3170                code: code.to_owned(),
3171                name: pascal_case(code),
3172                structured: true,
3173                payload: object
3174                    .get("properties")
3175                    .and_then(Value::as_object)
3176                    .and_then(|properties| properties.get("payload"))
3177                    .map(type_ir_from_schema),
3178                payload_required: required_fields(variant).contains("payload"),
3179            })
3180        })
3181        .collect::<Vec<_>>();
3182    variants.sort_by(|left, right| left.code.cmp(&right.code));
3183    variants
3184}
3185
3186fn error_variant_values(schema: &Value) -> BTreeMap<String, Value> {
3187    let variants = schema
3188        .get("oneOf")
3189        .and_then(Value::as_array)
3190        .into_iter()
3191        .flatten();
3192    variants
3193        .filter_map(|variant| {
3194            let code = if let Some(code) = variant.get("const").and_then(Value::as_str) {
3195                code.to_owned()
3196            } else {
3197                variant
3198                    .get("properties")
3199                    .and_then(Value::as_object)
3200                    .and_then(|properties| properties.get("code"))
3201                    .and_then(|code| code.get("const"))
3202                    .and_then(Value::as_str)
3203                    .map(ToOwned::to_owned)?
3204            };
3205            Some((code, variant.clone()))
3206        })
3207        .collect()
3208}
3209
3210fn required_fields(schema: &Value) -> BTreeSet<String> {
3211    schema
3212        .get("required")
3213        .and_then(Value::as_array)
3214        .into_iter()
3215        .flatten()
3216        .filter_map(Value::as_str)
3217        .map(ToOwned::to_owned)
3218        .collect()
3219}
3220
3221fn is_nullable(schema: &Value) -> bool {
3222    schema
3223        .get("type")
3224        .and_then(Value::as_array)
3225        .is_some_and(|types| types.iter().any(|value| value == "null"))
3226        || schema
3227            .get("anyOf")
3228            .and_then(Value::as_array)
3229            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
3230        || schema
3231            .get("oneOf")
3232            .and_then(Value::as_array)
3233            .is_some_and(|schemas| schemas.iter().any(is_null_schema))
3234}
3235
3236fn is_null_schema(schema: &Value) -> bool {
3237    schema.get("type").is_some_and(|value| value == "null")
3238        || schema.get("const").is_some_and(Value::is_null)
3239}
3240
3241fn schema_type_name(schema: &Value) -> Option<&str> {
3242    schema.get("type").and_then(|value| match value {
3243        Value::String(value) => Some(value.as_str()),
3244        Value::Array(values) => values
3245            .iter()
3246            .find_map(Value::as_str)
3247            .filter(|value| *value != "null"),
3248        _ => None,
3249    })
3250}
3251
3252fn canonical_json(value: &Value) -> String {
3253    serde_json::to_string(value).expect("JSON values are serializable")
3254}
3255
3256fn schema_constraints_signature(schema: &Value) -> String {
3257    let mut schema = schema.as_object().cloned().unwrap_or_default();
3258    for key in [
3259        "type",
3260        "properties",
3261        "required",
3262        "additionalProperties",
3263        "items",
3264    ] {
3265        schema.remove(key);
3266    }
3267    canonical_json(&Value::Object(schema))
3268}
3269
3270fn is_identifier(value: &str) -> bool {
3271    let mut characters = value.chars();
3272    characters
3273        .next()
3274        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
3275        && characters
3276            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
3277}
3278
3279fn is_rust_member_name(value: &str) -> bool {
3280    !matches!(snake_case(value).as_str(), "self" | "super" | "crate")
3281}
3282
3283fn is_rust_type_identifier(value: &str) -> bool {
3284    let mut characters = value.chars();
3285    let valid = characters
3286        .next()
3287        .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
3288        && characters.all(|character| character.is_ascii_alphanumeric() || character == '_');
3289    valid && value != "Self"
3290}
3291
3292fn is_generated_type_name(value: &str) -> bool {
3293    is_rust_type_identifier(value)
3294        && !matches!(
3295            value,
3296            "Self"
3297                | "String"
3298                | "Int64"
3299                | "Uint64"
3300                | "Bytes"
3301                | "Timestamp"
3302                | "Duration"
3303                | "OptionalValue"
3304                | "UnknownDomainError"
3305                | "InvocationContext"
3306                | "PluginDependencies"
3307                | "NativeRequestEndpoint"
3308                | "NativeRequestHandle"
3309                | "RequestCapability"
3310                | "RuntimeFailure"
3311                | "LocalBoxFuture"
3312        )
3313}
3314
3315fn is_generated_enum_variant(value: &str) -> bool {
3316    is_rust_type_identifier(value)
3317}
3318
3319fn pascal_case(value: &str) -> String {
3320    let mut output = String::new();
3321    for part in value.split(|character: char| !character.is_ascii_alphanumeric()) {
3322        if part.is_empty() || part.chars().all(char::is_numeric) {
3323            continue;
3324        }
3325        let mut chars = part.chars();
3326        if let Some(first) = chars.next() {
3327            output.extend(first.to_uppercase());
3328            output.push_str(chars.as_str());
3329        }
3330    }
3331    if output.is_empty() {
3332        "Value".to_owned()
3333    } else if output
3334        .chars()
3335        .next()
3336        .is_some_and(|character| character.is_ascii_digit())
3337    {
3338        format!("Value{output}")
3339    } else {
3340        output
3341    }
3342}
3343
3344fn snake_case(value: &str) -> String {
3345    let mut output = String::new();
3346    for (index, character) in value.chars().enumerate() {
3347        if character.is_ascii_alphanumeric() {
3348            if character.is_ascii_uppercase() && index != 0 {
3349                output.push('_');
3350            }
3351            output.push(character.to_ascii_lowercase());
3352        } else if !output.ends_with('_') {
3353            output.push('_');
3354        }
3355    }
3356    let output = output.trim_matches('_').to_owned();
3357    if output.is_empty() {
3358        "value".to_owned()
3359    } else {
3360        output
3361    }
3362}
3363
3364fn screaming_snake_case(value: &str) -> String {
3365    snake_case(value).to_ascii_uppercase()
3366}
3367
3368fn rust_field_name(value: &str) -> String {
3369    let name = snake_case(value);
3370    if matches!(name.as_str(), "self" | "super" | "crate") {
3371        return format!("{name}_");
3372    }
3373    if matches!(
3374        name.as_str(),
3375        "as" | "break"
3376            | "const"
3377            | "continue"
3378            | "crate"
3379            | "else"
3380            | "enum"
3381            | "extern"
3382            | "false"
3383            | "fn"
3384            | "for"
3385            | "if"
3386            | "impl"
3387            | "in"
3388            | "let"
3389            | "loop"
3390            | "match"
3391            | "mod"
3392            | "move"
3393            | "mut"
3394            | "pub"
3395            | "ref"
3396            | "return"
3397            | "self"
3398            | "Self"
3399            | "static"
3400            | "struct"
3401            | "super"
3402            | "trait"
3403            | "true"
3404            | "type"
3405            | "unsafe"
3406            | "use"
3407            | "where"
3408            | "while"
3409            | "async"
3410            | "await"
3411            | "dyn"
3412    ) {
3413        format!("r#{name}")
3414    } else {
3415        name
3416    }
3417}
3418
3419fn quote_string(value: &str) -> String {
3420    serde_json::to_string(value).expect("string is serializable")
3421}
3422
3423fn typescript_property_name(value: &str) -> String {
3424    let mut characters = value.chars();
3425    let valid_identifier = characters
3426        .next()
3427        .is_some_and(|character| character.is_ascii_alphabetic() || matches!(character, '_' | '$'))
3428        && characters
3429            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$'));
3430    if valid_identifier {
3431        value.to_owned()
3432    } else {
3433        quote_string(value)
3434    }
3435}
3436
3437struct RustTypes {
3438    declarations: Vec<String>,
3439    declared: BTreeSet<String>,
3440}
3441
3442impl RustTypes {
3443    fn new() -> Self {
3444        Self {
3445            declarations: Vec::new(),
3446            declared: BTreeSet::new(),
3447        }
3448    }
3449
3450    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3451        if !self.declared.insert(name.to_owned()) {
3452            return name.to_owned();
3453        }
3454        let placeholder = self.declarations.len();
3455        self.declarations.push(String::new());
3456        let mut rendered_fields = Vec::new();
3457        for field in fields {
3458            let type_name = self.type_for_non_null(
3459                field.ty.non_null(),
3460                &format!("{name}{}", pascal_case(&field.name)),
3461            );
3462            let field_type = if field.required {
3463                if field.ty.is_nullable() {
3464                    format!("Option<{type_name}>")
3465                } else {
3466                    type_name
3467                }
3468            } else if field.ty.is_nullable() {
3469                format!("OptionalValue<{type_name}>")
3470            } else {
3471                format!("Option<{type_name}>")
3472            };
3473            let mut attributes = vec![format!(
3474                "    #[serde(rename = {})]",
3475                quote_string(&field.name)
3476            )];
3477            if !field.required {
3478                if field.ty.is_nullable() {
3479                    attributes.push("    #[serde(default)]".to_owned());
3480                    attributes
3481                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
3482                    attributes.push(
3483                        "    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_optional_value\")]"
3484                            .to_owned(),
3485                    );
3486                } else {
3487                    attributes
3488                        .push("    #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
3489                }
3490            }
3491            if field.required {
3492                attributes
3493                    .push("    #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_required\")]".to_owned());
3494            }
3495            rendered_fields.push(format!(
3496                "{}\n    pub {}: {field_type},",
3497                attributes.join("\n"),
3498                rust_field_name(&field.name)
3499            ));
3500        }
3501        let debug_impl = rust_debug::render(
3502            name,
3503            fields.iter().map(|field| {
3504                (
3505                    field.name.as_str(),
3506                    rust_field_name(&field.name),
3507                    field.sensitive,
3508                )
3509            }),
3510        );
3511        let derives = if fields.is_empty() {
3512            "Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize"
3513        } else if debug_impl.is_some() {
3514            "Clone, PartialEq, serde::Serialize, serde::Deserialize"
3515        } else {
3516            "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
3517        };
3518        self.declarations[placeholder] = format!(
3519            "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
3520            rendered_fields.join("\n"),
3521            debug_impl = debug_impl.unwrap_or_default(),
3522        );
3523        name.to_owned()
3524    }
3525
3526    fn enum_type(&mut self, name: &str, values: &[String]) -> String {
3527        if !self.declared.insert(name.to_owned()) {
3528            return name.to_owned();
3529        }
3530        let variants = values
3531            .iter()
3532            .map(|value| {
3533                format!(
3534                    "    #[serde(rename = {})]\n    {},",
3535                    quote_string(value),
3536                    pascal_case(value)
3537                )
3538            })
3539            .collect::<Vec<_>>();
3540        self.declarations.push(format!(
3541            "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
3542            variants.join("\n")
3543        ));
3544        name.to_owned()
3545    }
3546
3547    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3548        let base = self.type_for_non_null(ty.non_null(), nested_name);
3549        if ty.is_nullable() {
3550            format!("Option<{base}>")
3551        } else {
3552            base
3553        }
3554    }
3555
3556    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3557        match ty {
3558            TypeIr::Any => "serde_json::Value".to_owned(),
3559            TypeIr::String => "String".to_owned(),
3560            TypeIr::RawJson => "RawJson".to_owned(),
3561            TypeIr::Enum { name, values } => {
3562                self.enum_type(name.as_deref().unwrap_or(nested_name), values)
3563            }
3564            TypeIr::Int64 => "Int64".to_owned(),
3565            TypeIr::Uint64 => "Uint64".to_owned(),
3566            TypeIr::Bytes => "Bytes".to_owned(),
3567            TypeIr::Timestamp => "Timestamp".to_owned(),
3568            TypeIr::Duration => "Duration".to_owned(),
3569            TypeIr::Integer => "i64".to_owned(),
3570            TypeIr::Number => "f64".to_owned(),
3571            TypeIr::Boolean => "bool".to_owned(),
3572            TypeIr::Null => "()".to_owned(),
3573            TypeIr::Array(items) => {
3574                format!(
3575                    "Vec<{}>",
3576                    self.type_for(items, &format!("{nested_name}Item"))
3577                )
3578            }
3579            TypeIr::Object {
3580                name,
3581                fields,
3582                additional,
3583            } => {
3584                let nested_name = name.as_deref().unwrap_or(nested_name);
3585                if fields.is_empty() {
3586                    match additional {
3587                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3588                        ObjectAdditionalIr::Any => {
3589                            "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
3590                        }
3591                        ObjectAdditionalIr::Typed(values) => format!(
3592                            "std::collections::BTreeMap<String, {}>",
3593                            self.type_for(values, &format!("{nested_name}Value"))
3594                        ),
3595                    }
3596                } else {
3597                    self.object(nested_name, fields)
3598                }
3599            }
3600            TypeIr::Nullable(inner) => {
3601                format!("Option<{}>", self.type_for_non_null(inner, nested_name))
3602            }
3603        }
3604    }
3605}
3606
3607struct TypeScriptTypes {
3608    declarations: Vec<String>,
3609    declared: BTreeSet<String>,
3610}
3611
3612impl TypeScriptTypes {
3613    fn new() -> Self {
3614        Self {
3615            declarations: Vec::new(),
3616            declared: BTreeSet::new(),
3617        }
3618    }
3619
3620    fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3621        if !self.declared.insert(name.to_owned()) {
3622            return name.to_owned();
3623        }
3624        let placeholder = self.declarations.len();
3625        self.declarations.push(String::new());
3626        let mut rendered_fields = Vec::new();
3627        for field in fields {
3628            let type_name = self.type_for_non_null(
3629                field.ty.non_null(),
3630                &format!("{name}{}", pascal_case(&field.name)),
3631            );
3632            let optional = if field.required { "" } else { "?" };
3633            let field_type = if field.ty.is_nullable() {
3634                format!("{type_name} | null")
3635            } else {
3636                type_name
3637            };
3638            rendered_fields.push(format!(
3639                "  {}{optional}: {field_type};",
3640                typescript_property_name(&field.name)
3641            ));
3642        }
3643        self.declarations[placeholder] = format!(
3644            "export interface {name} {{\n{}\n}}\n",
3645            rendered_fields.join("\n")
3646        );
3647        name.to_owned()
3648    }
3649
3650    fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3651        let base = self.type_for_non_null(ty.non_null(), nested_name);
3652        if ty.is_nullable() {
3653            format!("{base} | null")
3654        } else {
3655            base
3656        }
3657    }
3658
3659    fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3660        match ty {
3661            TypeIr::Any => "unknown".to_owned(),
3662            TypeIr::String | TypeIr::RawJson => "string".to_owned(),
3663            TypeIr::Int64 => "Int64".to_owned(),
3664            TypeIr::Uint64 => "Uint64".to_owned(),
3665            TypeIr::Bytes => "Bytes".to_owned(),
3666            TypeIr::Timestamp => "Timestamp".to_owned(),
3667            TypeIr::Duration => "Duration".to_owned(),
3668            TypeIr::Integer | TypeIr::Number => "number".to_owned(),
3669            TypeIr::Boolean => "boolean".to_owned(),
3670            TypeIr::Null => "null".to_owned(),
3671            TypeIr::Enum { values, .. } => values
3672                .iter()
3673                .map(|value| quote_string(value))
3674                .collect::<Vec<_>>()
3675                .join(" | "),
3676            TypeIr::Array(items) => {
3677                format!(
3678                    "Array<{}>",
3679                    self.type_for(items, &format!("{nested_name}Item"))
3680                )
3681            }
3682            TypeIr::Object {
3683                name,
3684                fields,
3685                additional,
3686            } => {
3687                let nested_name = name.as_deref().unwrap_or(nested_name);
3688                if fields.is_empty() {
3689                    match additional {
3690                        ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3691                        ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
3692                        ObjectAdditionalIr::Typed(values) => format!(
3693                            "Record<string, {}>",
3694                            self.type_for(values, &format!("{nested_name}Value"))
3695                        ),
3696                    }
3697                } else {
3698                    self.object(nested_name, fields)
3699                }
3700            }
3701            TypeIr::Nullable(inner) => {
3702                format!("{} | null", self.type_for_non_null(inner, nested_name))
3703            }
3704        }
3705    }
3706}
3707
3708#[allow(clippy::too_many_lines)]
3709fn generate_rust(contract: &ContractIr) -> String {
3710    let capability_name = pascal_case(
3711        contract
3712            .capability_id
3713            .split('@')
3714            .next()
3715            .and_then(|identity| identity.rsplit('.').next())
3716            .unwrap_or("Capability"),
3717    );
3718    let capability_const = screaming_snake_case(&capability_name);
3719    let native_support_name = format!("__LensoNativeSupport{capability_name}");
3720    let mut types = RustTypes::new();
3721    let mut operation_rows = Vec::new();
3722    let mut stream_operation_rows = Vec::new();
3723    let mut event_operation_rows = Vec::new();
3724    let mut operation_markers = Vec::new();
3725    let mut provider_methods = Vec::new();
3726    let mut provider_lowering_methods = Vec::new();
3727    let mut object_provider_lowering_methods = Vec::new();
3728    let mut trait_object_provider_lowering_methods = Vec::new();
3729    let mut provider_result_conversions = Vec::new();
3730    let mut endpoint_arms = Vec::new();
3731    let mut stream_endpoint_arms = Vec::new();
3732    let mut event_endpoint_arms = Vec::new();
3733    let mut client_fields = Vec::new();
3734    let mut client_initializers = Vec::new();
3735    let mut many_client_initializers = Vec::new();
3736    let mut client_methods = Vec::new();
3737    let mut invocation_errors = Vec::new();
3738    let mut error_codecs = Vec::new();
3739    let mut wire_codecs = Vec::new();
3740
3741    for operation in &contract.operations {
3742        let operation_name = pascal_case(&operation.name);
3743        let request_name = format!("{operation_name}Request");
3744        let response_name = format!("{operation_name}Response");
3745        let error_name = format!("{operation_name}Error");
3746        let marker_name = if contract.operations.len() == 1 {
3747            capability_name.clone()
3748        } else {
3749            format!("{capability_name}{operation_name}")
3750        };
3751        let request_type = types.type_for(&operation.request, &request_name);
3752        let response_type = types.type_for(&operation.response, &response_name);
3753        let known_errors = &operation.domain_errors;
3754        let error_definition = if known_errors.is_empty() {
3755            format!(
3756                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n    Unknown(UnknownDomainError),\n}}\n"
3757            )
3758        } else {
3759            let variants = known_errors
3760                .iter()
3761                .map(|variant| {
3762                    if let Some(payload) = &variant.payload {
3763                        let payload_name = format!("{error_name}{}Payload", variant.name);
3764                        let payload_type =
3765                            types.type_for_non_null(payload.non_null(), &payload_name);
3766                        let payload_type = if variant.payload_required {
3767                            if payload.is_nullable() {
3768                                format!("Option<{payload_type}>")
3769                            } else {
3770                                payload_type
3771                            }
3772                        } else if payload.is_nullable() {
3773                            format!("OptionalValue<{payload_type}>")
3774                        } else {
3775                            format!("Option<{payload_type}>")
3776                        };
3777                        format!("    {} {{ payload: {payload_type} }},", variant.name)
3778                    } else {
3779                        format!("    {},", variant.name)
3780                    }
3781                })
3782                .chain(std::iter::once(
3783                    "    Unknown(UnknownDomainError),".to_owned(),
3784                ))
3785                .collect::<Vec<_>>();
3786            format!(
3787                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
3788                variants.join("\n")
3789            )
3790        };
3791        types.declarations.push(error_definition);
3792        error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
3793        wire_codecs.push(generate_rust_wire_codecs(
3794            &operation.name,
3795            &request_type,
3796            &response_type,
3797            &error_name,
3798        ));
3799        if operation.interaction == "event" {
3800            wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
3801        }
3802        let operation_const = screaming_snake_case(&operation.name);
3803        operation_markers.push(match operation.interaction.as_str() {
3804            "request" => {
3805                let provider_method = rust_field_name(&operation.name);
3806                format!(
3807            "#[derive(Debug)]\npub struct {marker_name};\nimpl RequestCapability for {marker_name} {{\n    type Request = {request_type};\n    type Response = {response_type};\n    type DomainError = {error_name};\n    const ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n\n    fn invoke_native(endpoint: &dyn NativeRequestEndpoint, operation: &str, request: Self::Request, context: InvocationContext) -> NativeRequestFuture<Self> {{\n        if operation != {operation_const}_OPERATION {{\n            return lenso_kernel::invoke_typed_or_erased_native_request::<Self>(endpoint, operation, request, context);\n        }}\n        let Some(typed_endpoint) = endpoint\n            .typed_endpoint()\n            .and_then(|endpoint| endpoint.downcast_ref::<{capability_name}RequestEndpoint>())\n        else {{\n            return lenso_kernel::invoke_typed_or_erased_native_request::<Self>(endpoint, operation, request, context);\n        }};\n        Rc::clone(&typed_endpoint.provider).{provider_method}(context, request)\n    }}\n}}\n"
3808                )
3809            }
3810            "stream" => format!(
3811                "#[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"
3812            ),
3813            "event" => format!(
3814                "#[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"
3815            ),
3816            _ => unreachable!("Descriptor validation restricts interactions"),
3817        });
3818        if operation.interaction == "request" {
3819            let invocation_error_name = if contract.operations.len() == 1 {
3820                format!("{capability_name}InvocationError")
3821            } else {
3822                format!("{capability_name}{operation_name}InvocationError")
3823            };
3824            operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
3825            provider_methods.push(format!(
3826                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
3827                rust_field_name(&operation.name),
3828            ));
3829            let field = rust_field_name(&operation.name);
3830            let conversion = format!("__LensoInto{capability_name}{operation_name}Result");
3831            provider_result_conversions.push(format!(
3832                "#[doc(hidden)]\npub trait {conversion} {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure>;\n}}\nimpl {conversion} for Result<{response_type}, {error_name}> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{ Ok(self) }}\n}}\nimpl {conversion} for Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{ self }}\n}}\nimpl {conversion} for Result<{response_type}, lenso_plugin_authoring::PluginError<{error_name}, RuntimeFailure>> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{\n        match self {{\n            Ok(value) => Ok(Ok(value)),\n            Err(lenso_plugin_authoring::PluginError::Domain(error)) => Ok(Err(error)),\n            Err(lenso_plugin_authoring::PluginError::Runtime(error)) => Err(error),\n        }}\n    }}\n}}\nimpl {conversion} for Result<{response_type}, {invocation_error_name}> {{\n    fn __lenso_into_result(self) -> Result<Result<{response_type}, {error_name}>, RuntimeFailure> {{\n        match self {{\n            Ok(value) => Ok(Ok(value)),\n            Err({invocation_error_name}::Domain(error)) => Ok(Err(error)),\n            Err({invocation_error_name}::Runtime(error)) => Err(error),\n        }}\n    }}\n}}\n"
3833            ));
3834            provider_lowering_methods.push(format!(
3835                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let plugin = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$plugin>::{field}(&plugin, context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3836            ));
3837            object_provider_lowering_methods.push(format!(
3838                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                let result = <$plugin>::{field}(plugin.as_ref(), context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3839            ));
3840            trait_object_provider_lowering_methods.push(format!(
3841                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                <$plugin as $crate::{capability_name}Provider>::{field}(plugin.as_ref(), context, request).await\n            }})\n        }}"
3842            ));
3843            endpoint_arms.push(format!(
3844                "            {operation_const}_OPERATION => {{\n                let Ok(request) = request.downcast::<{request_type}>() else {{\n                    return Box::pin(futures::future::ready(Err(RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }})));\n                }};\n                let invocation = Rc::clone(&self.provider).{}(context, *request);\n                Box::pin(async move {{\n                    invocation.await.map(|result| {{\n                        result\n                            .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                            .map_err(|error| Box::new(error) as Box<dyn std::any::Any>)\n                    }})\n                }})\n            }}",
3845                rust_field_name(&operation.name),
3846            ));
3847            let field = rust_field_name(&operation.name);
3848            client_fields.push(format!("    {field}: NativeRequestHandle<{marker_name}>,"));
3849            client_initializers.push(format!(
3850                "            {field}: dependencies.one::<{marker_name}>()?,"
3851            ));
3852            many_client_initializers.push(format!(
3853                "                    {field}: binding.handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3854            ));
3855            client_methods.push(format!(
3856                "    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    }}",
3857                screaming_snake_case(&operation.name),
3858                screaming_snake_case(&operation.name),
3859            ));
3860            invocation_errors.push(format!(
3861                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
3862            ));
3863        } else if operation.interaction == "stream" {
3864            let invocation_error_name = if contract.operations.len() == 1 {
3865                format!("{capability_name}InvocationError")
3866            } else {
3867                format!("{capability_name}{operation_name}InvocationError")
3868            };
3869            stream_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
3870            provider_methods.push(format!(
3871                "    fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
3872                rust_field_name(&operation.name)
3873            ));
3874            let field = rust_field_name(&operation.name);
3875            let conversion = format!("__LensoInto{capability_name}{operation_name}StreamResult");
3876            provider_result_conversions.push(format!(
3877                "#[doc(hidden)]\npub trait {conversion} {{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}>;\n}}\nimpl<S> {conversion} for Result<S, {error_name}>\nwhere\n    S: NativeStreamSession + 'static,\n{{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}> {{\n        self.map(|stream| Box::new(stream) as Box<dyn NativeStreamSession>)\n            .map_err({invocation_error_name}::Domain)\n    }}\n}}\nimpl<S> {conversion} for Result<S, lenso_plugin_authoring::PluginError<{error_name}, RuntimeFailure>>\nwhere\n    S: NativeStreamSession + 'static,\n{{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}> {{\n        match self {{\n            Ok(stream) => Ok(Box::new(stream) as Box<dyn NativeStreamSession>),\n            Err(lenso_plugin_authoring::PluginError::Domain(error)) => Err({invocation_error_name}::Domain(error)),\n            Err(lenso_plugin_authoring::PluginError::Runtime(error)) => Err({invocation_error_name}::Runtime(error)),\n        }}\n    }}\n}}\nimpl<S> {conversion} for Result<S, {invocation_error_name}>\nwhere\n    S: NativeStreamSession + 'static,\n{{\n    fn __lenso_into_result(self) -> Result<Box<dyn NativeStreamSession>, {invocation_error_name}> {{\n        self.map(|stream| Box::new(stream) as Box<dyn NativeStreamSession>)\n    }}\n}}\n"
3878            ));
3879            provider_lowering_methods.push(format!(
3880                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n            let plugin = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$plugin>::{field}(&plugin, context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3881            ));
3882            object_provider_lowering_methods.push(format!(
3883                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get().map_err($crate::{invocation_error_name}::Runtime)?;\n                let result = <$plugin>::{field}(plugin.as_ref(), context, request).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3884            ));
3885            trait_object_provider_lowering_methods.push(format!(
3886                "        fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get().map_err($crate::{invocation_error_name}::Runtime)?;\n                <$plugin as $crate::{capability_name}Provider>::{field}(plugin.as_ref(), context, request).await\n            }})\n        }}"
3887            ));
3888            stream_endpoint_arms.push(format!(
3889                "            {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            }}",
3890                rust_field_name(&operation.name),
3891            ));
3892            let field = rust_field_name(&operation.name);
3893            client_fields.push(format!("    {field}: NativeStreamHandle<{marker_name}>,"));
3894            client_initializers.push(format!(
3895                "            {field}: dependencies.one_stream::<{marker_name}>()?,"
3896            ));
3897            many_client_initializers.push(format!(
3898                "                    {field}: binding.stream_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3899            ));
3900            client_methods.push(format!(
3901                "    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    }}"
3902            ));
3903            invocation_errors.push(format!(
3904                "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n    Domain({error_name}),\n    Runtime(RuntimeFailure),\n}}\n"
3905            ));
3906        } else {
3907            event_operation_rows.push(format!("        {operation_const}_OPERATION,\n"));
3908            provider_methods.push(format!(
3909                "    fn {}(&self, context: InvocationContext, event: {request_type}) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;",
3910                rust_field_name(&operation.name)
3911            ));
3912            let field = rust_field_name(&operation.name);
3913            let conversion = format!("__LensoInto{capability_name}{operation_name}EventResult");
3914            provider_result_conversions.push(format!(
3915                "#[doc(hidden)]\npub trait {conversion} {{\n    fn __lenso_into_result(self) -> Result<(), RuntimeFailure>;\n}}\nimpl {conversion} for () {{\n    fn __lenso_into_result(self) -> Result<(), RuntimeFailure> {{ Ok(()) }}\n}}\nimpl {conversion} for Result<(), RuntimeFailure> {{\n    fn __lenso_into_result(self) -> Result<(), RuntimeFailure> {{ self }}\n}}\n"
3916            ));
3917            provider_lowering_methods.push(format!(
3918                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let plugin = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let result = <$plugin>::{field}(&plugin, context, event).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3919            ));
3920            object_provider_lowering_methods.push(format!(
3921                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                let result = <$plugin>::{field}(plugin.as_ref(), context, event).await;\n                $crate::{conversion}::__lenso_into_result(result)\n            }})\n        }}"
3922            ));
3923            trait_object_provider_lowering_methods.push(format!(
3924                "        fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n            let object = self.clone();\n            ::std::boxed::Box::pin(async move {{\n                let plugin = object.get()?;\n                <$plugin as $crate::{capability_name}Provider>::{field}(plugin.as_ref(), context, event).await\n            }})\n        }}"
3925            ));
3926            event_endpoint_arms.push(format!(
3927                "            {operation_const}_OPERATION => {{\n                let Ok(event) = event.downcast::<{request_type}>() else {{\n                    return Box::pin(futures::future::ready(Err(RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }})));\n                }};\n                Rc::clone(&self.provider).{}(context, *event)\n            }}",
3928                rust_field_name(&operation.name),
3929            ));
3930            let field = rust_field_name(&operation.name);
3931            client_fields.push(format!("    {field}: NativeEventHandle<{marker_name}>,"));
3932            client_initializers.push(format!(
3933                "            {field}: dependencies.many_event::<{marker_name}>()?,"
3934            ));
3935            many_client_initializers.push(format!(
3936                "                    {field}: binding.event_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3937            ));
3938            client_methods.push(format!(
3939                "    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    }}"
3940            ));
3941        }
3942    }
3943
3944    let request_endpoint_impl = if operation_rows.is_empty() {
3945        String::new()
3946    } else {
3947        format!(
3948            "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",
3949            operation_rows.concat(),
3950            endpoint_arms.join(",\n")
3951        )
3952    };
3953    let stream_endpoint_impl = if stream_operation_rows.is_empty() {
3954        String::new()
3955    } else {
3956        format!(
3957            "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",
3958            stream_operation_rows.concat(),
3959            stream_endpoint_arms.join(",\n")
3960        )
3961    };
3962    let event_endpoint_impl = if event_operation_rows.is_empty() {
3963        String::new()
3964    } else {
3965        format!(
3966            "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",
3967            event_operation_rows.concat(),
3968            event_endpoint_arms.join(",\n")
3969        )
3970    };
3971    let mut output = String::new();
3972    output.push_str(GENERATED_HEADER);
3973    let has_request_operations = !operation_rows.is_empty();
3974    let has_stream_operations = !stream_operation_rows.is_empty();
3975    let has_event_operations = !event_operation_rows.is_empty();
3976    let mut kernel_imports = vec!["InvocationContext", "PluginDependencies", "RuntimeFailure"];
3977    if has_request_operations {
3978        kernel_imports.extend([
3979            "NativeRequestFuture",
3980            "NativeRequestEndpoint",
3981            "NativeRequestHandle",
3982            "RequestCapability",
3983        ]);
3984    }
3985    if has_stream_operations {
3986        kernel_imports.extend([
3987            "NativeStream",
3988            "NativeStreamEndpoint",
3989            "NativeStreamHandle",
3990            "NativeStreamSession",
3991            "StreamCapability",
3992            "StreamEvent",
3993        ]);
3994    }
3995    if has_event_operations {
3996        kernel_imports.extend([
3997            "EventCapability",
3998            "EventPublishResult",
3999            "NativeEventEndpoint",
4000            "NativeEventHandle",
4001        ]);
4002    }
4003    kernel_imports.sort_unstable();
4004    writeln!(
4005        output,
4006        "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
4007        kernel_imports.join(", ")
4008    )
4009    .expect("writing to a String cannot fail");
4010    output.push_str(
4011        "use lenso_plugin_authoring::{BoundCapabilityClient, CapabilityClient, CapabilityClientMany, CapabilityReference};\n",
4012    );
4013    writeln!(
4014        output,
4015        "pub const CAPABILITY_ID: &str = {};",
4016        quote_string(&contract.capability_id)
4017    )
4018    .expect("writing to a String cannot fail");
4019    writeln!(
4020        output,
4021        "pub const DESCRIPTOR_VERSION: &str = {};",
4022        quote_string(&contract.version)
4023    )
4024    .expect("writing to a String cannot fail");
4025    writeln!(
4026        output,
4027        "pub const DESCRIPTOR_DIGEST: &str = {};",
4028        quote_string(&contract.descriptor_digest)
4029    )
4030    .expect("writing to a String cannot fail");
4031    writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
4032        .expect("writing to a String cannot fail");
4033    writeln!(
4034        output,
4035        "pub const CROSS_LANE_TRANSFER: bool = {};",
4036        contract.cross_lane_transfer
4037    )
4038    .expect("writing to a String cannot fail");
4039    writeln!(
4040        output,
4041        "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
4042    )
4043    .expect("writing to a String cannot fail");
4044    write!(
4045        output,
4046        "pub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\npub const {capability_const}_DESCRIPTOR_DIGEST: &str = DESCRIPTOR_DIGEST;\npub const {capability_const}_CONTRACT: CapabilityReference<{capability_name}Client> = CapabilityReference::new(CAPABILITY_ID, DESCRIPTOR_VERSION, DESCRIPTOR_DIGEST);\n\n"
4047    )
4048    .expect("writing to a String cannot fail");
4049
4050    let capability_macro_name = snake_case(&capability_name);
4051    let client_macro_name = snake_case(&format!("{capability_name}Client"));
4052    let operations = contract
4053        .operations
4054        .iter()
4055        .map(|operation| Value::String(operation.name.clone()))
4056        .collect::<Vec<_>>();
4057    let operation_kinds = contract
4058        .operations
4059        .iter()
4060        .filter(|operation| operation.interaction != "request")
4061        .map(|operation| {
4062            (
4063                operation.name.clone(),
4064                Value::String(operation.interaction.clone()),
4065            )
4066        })
4067        .collect::<Map<_, _>>();
4068    let (queue_capacity, max_concurrency) = contract.request_admission.unwrap_or((0, 1));
4069    let provided_fragment = canonical_json(&serde_json::json!({
4070        "capability_id": contract.capability_id,
4071        "descriptor_version": contract.version,
4072        "operations": operations,
4073        "operation_kinds": operation_kinds,
4074        "default_admission": {
4075            "queue_capacity": queue_capacity,
4076            "max_concurrency": max_concurrency
4077        },
4078        "operation_admissions": {},
4079        "event_admission": null,
4080        "cross_lane_transfer": contract.cross_lane_transfer
4081    }));
4082    let required_fragment = canonical_json(&serde_json::json!({
4083        "capability_id": contract.capability_id,
4084        "descriptor_version": contract.version,
4085        "cardinality": "one"
4086    }));
4087    let required_many_fragment = canonical_json(&serde_json::json!({
4088        "capability_id": contract.capability_id,
4089        "descriptor_version": contract.version,
4090        "cardinality": "many"
4091    }));
4092    let named_requirement_suffix = |cardinality: &str| {
4093        format!(
4094            ",\"capability_id\":{},\"descriptor_version\":{},\"cardinality\":{}}}",
4095            quote_string(&contract.capability_id),
4096            quote_string(&contract.version),
4097            quote_string(cardinality),
4098        )
4099    };
4100    let required_named_suffix = named_requirement_suffix("one");
4101    let required_optional_named_suffix = named_requirement_suffix("optional");
4102    let required_many_named_suffix = named_requirement_suffix("many");
4103    writeln!(
4104        output,
4105        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_provided_{capability_macro_name} {{ () => {{ {} }}; }}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_required_{client_macro_name} {{\n    () => {{ {} }};\n    ($requirement_id:literal) => {{ concat!(\"{{\\\"requirement_id\\\":\", stringify!($requirement_id), {}) }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_required_optional_{client_macro_name} {{\n    ($requirement_id:literal) => {{ concat!(\"{{\\\"requirement_id\\\":\", stringify!($requirement_id), {}) }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_required_many_{client_macro_name} {{\n    () => {{ {} }};\n    ($requirement_id:literal) => {{ concat!(\"{{\\\"requirement_id\\\":\", stringify!($requirement_id), {}) }};\n}}\n",
4106        quote_string(&provided_fragment),
4107        quote_string(&required_fragment),
4108        quote_string(&required_named_suffix),
4109        quote_string(&required_optional_named_suffix),
4110        quote_string(&required_many_fragment),
4111        quote_string(&required_many_named_suffix),
4112    )
4113    .expect("writing to a String cannot fail");
4114    for operation in &contract.operations {
4115        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4116        writeln!(
4117            output,
4118            "pub const {operation_const}: &str = {};",
4119            quote_string(&operation.name)
4120        )
4121        .expect("writing to a String cannot fail");
4122    }
4123    writeln!(
4124        output,
4125        "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
4126        rust_runtime_types(contract).join(", ")
4127    )
4128    .expect("writing to a String cannot fail");
4129    for declaration in types.declarations {
4130        output.push_str(&declaration);
4131        output.push('\n');
4132    }
4133    for marker in operation_markers {
4134        output.push_str(&marker);
4135        output.push('\n');
4136    }
4137    for codec in error_codecs {
4138        output.push_str(&codec);
4139        output.push('\n');
4140    }
4141    for codec in wire_codecs {
4142        output.push_str(&codec);
4143        output.push('\n');
4144    }
4145    for conversion in provider_result_conversions {
4146        output.push_str(&conversion);
4147        output.push('\n');
4148    }
4149    write!(
4150        output,
4151        "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
4152        provider_methods.join("\n")
4153    )
4154    .expect("writing to a String cannot fail");
4155    writeln!(
4156        output,
4157        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_{capability_macro_name} {{\n    ($plugin:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $plugin {{\n{}\n        }}\n    }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_object_{capability_macro_name} {{\n    ($object:ty, $plugin:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $object {{\n{}\n        }}\n    }};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_trait_object_{capability_macro_name} {{\n    ($object:ty, $plugin:ty, $support:path) => {{\n        use $support as {native_support_name};\n        impl $crate::{capability_name}Provider for $object {{\n{}\n        }}\n    }};\n}}\n",
4158        provider_lowering_methods.join("\n"),
4159        object_provider_lowering_methods.join("\n"),
4160        trait_object_provider_lowering_methods.join("\n")
4161    )
4162    .expect("writing to a String cannot fail");
4163    if has_request_operations {
4164        write!(
4165            output,
4166            "#[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"
4167        )
4168        .expect("writing to a String cannot fail");
4169    } else {
4170        write!(
4171            output,
4172            "#[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"
4173        )
4174        .expect("writing to a String cannot fail");
4175    }
4176    output.push_str(&request_endpoint_impl);
4177    output.push_str(&stream_endpoint_impl);
4178    output.push_str(&event_endpoint_impl);
4179    let request_endpoint_value = has_request_operations.then_some(
4180        "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeRequestEndpoint>",
4181    );
4182    let stream_endpoint_value = has_stream_operations.then_some(
4183        "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeStreamEndpoint>",
4184    );
4185    let event_endpoint_value = has_event_operations
4186        .then_some("endpoint as ::std::rc::Rc<dyn __LensoNativeSupport::NativeEventEndpoint>");
4187    writeln!(
4188        output,
4189        "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_endpoints_{capability_macro_name} {{\n    ($provider:expr, $support:path) => {{{{\n        use $support as __LensoNativeSupport;\n        let endpoint = ::std::rc::Rc::new($crate::{capability_name}Endpoint::new($provider));\n        (\n            vec![{}],\n            vec![{}],\n            vec![{}],\n        )\n    }}}};\n}}\n\n#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_provide_{capability_macro_name} {{\n    ($provider:expr, $lifecycle:expr, $support:path) => {{{{\n        use $support as __LensoNativeSupport;\n        let (request_endpoints, stream_endpoints, event_endpoints) =\n            $crate::__lenso_native_endpoints_{capability_macro_name}!($provider, $support);\n        __LensoNativeSupport::NativePluginInstance::with_all_endpoints(\n            request_endpoints,\n            stream_endpoints,\n            event_endpoints,\n            $lifecycle,\n        )\n    }}}};\n}}\n",
4190        request_endpoint_value.unwrap_or_default(),
4191        stream_endpoint_value.unwrap_or_default(),
4192        event_endpoint_value.unwrap_or_default(),
4193    )
4194    .expect("writing to a String cannot fail");
4195    let new_method = if contract.operations.len() == 1 {
4196        let field = rust_field_name(&contract.operations[0].name);
4197        let marker = &capability_name;
4198        match contract.operations[0].interaction.as_str() {
4199            "request" => format!(
4200                "    pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
4201            ),
4202            "stream" => format!(
4203                "    pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
4204            ),
4205            "event" => format!(
4206                "    pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n        Self {{ {field}: handle }}\n    }}\n\n"
4207            ),
4208            _ => unreachable!("Descriptor validation restricts interactions"),
4209        }
4210    } else {
4211        String::new()
4212    };
4213    write!(
4214        output,
4215        "#[derive(Clone, Debug)]\npub struct {capability_name}Client {{\n{}\n}}\nimpl {capability_name}Client {{\n{}    pub fn from_dependencies(dependencies: &PluginDependencies) -> Result<Self, RuntimeFailure> {{\n        <Self as CapabilityClient>::from_dependencies(dependencies)\n    }}\n\n    pub fn from_requirement(\n        dependencies: &PluginDependencies,\n        requirement_id: &str,\n    ) -> Result<Self, RuntimeFailure> {{\n        <Self as CapabilityClient>::from_requirement(dependencies, requirement_id)\n    }}\n\n{}\n}}\n\nimpl CapabilityClient for {capability_name}Client {{\n    type Dependencies = PluginDependencies;\n    type Error = RuntimeFailure;\n\n    const CAPABILITY_ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n\n    fn from_dependencies(dependencies: &PluginDependencies) -> Result<Self, RuntimeFailure> {{\n        Ok(Self {{\n{}\n        }})\n    }}\n\n    fn from_requirement(\n        dependencies: &PluginDependencies,\n        requirement_id: &str,\n    ) -> Result<Self, RuntimeFailure> {{\n        let dependencies = dependencies.requirement(requirement_id)?;\n        Self::from_dependencies(&dependencies)\n    }}\n\n    fn already_connected() -> RuntimeFailure {{\n        RuntimeFailure::PluginFailure {{\n            detail: format!(\"Capability Port {{CAPABILITY_ID}} was connected more than once\"),\n        }}\n    }}\n}}\n\nimpl CapabilityClientMany for {capability_name}Client {{\n    fn many_from_dependencies(\n        dependencies: &PluginDependencies,\n    ) -> Result<Vec<BoundCapabilityClient<Self>>, RuntimeFailure> {{\n        dependencies\n            .bindings()\n            .iter()\n            .filter(|binding| binding.capability_id() == CAPABILITY_ID)\n            .map(|binding| {{\n                Ok(BoundCapabilityClient::new(\n                    binding.provider_instance(),\n                    Self {{\n{}\n                    }},\n                ))\n            }})\n            .collect()\n    }}\n\n    fn many_from_requirement(\n        dependencies: &PluginDependencies,\n        requirement_id: &str,\n    ) -> Result<Vec<BoundCapabilityClient<Self>>, RuntimeFailure> {{\n        let dependencies = dependencies.requirement(requirement_id)?;\n        Self::many_from_dependencies(&dependencies)\n    }}\n}}\n\n",
4216        client_fields.join("\n"),
4217        new_method,
4218        client_methods.join("\n\n"),
4219        client_initializers.join("\n"),
4220        many_client_initializers.join("\n")
4221    )
4222    .expect("writing to a String cannot fail");
4223    for error in invocation_errors {
4224        output.push_str(&error);
4225    }
4226    format!("{}\n", output.trim_end())
4227}
4228
4229#[allow(clippy::too_many_lines)]
4230fn generate_rust_plugin(contract: &ContractIr) -> Result<String, CodegenError> {
4231    let capability_name = pascal_case(
4232        contract
4233            .capability_id
4234            .split('@')
4235            .next()
4236            .and_then(|identity| identity.rsplit('.').next())
4237            .unwrap_or("Capability"),
4238    );
4239    let capability_const = screaming_snake_case(&capability_name);
4240    let mut types = RustTypes::new();
4241    let mut methods = Vec::new();
4242    let mut provider_methods = Vec::new();
4243    let mut dispatch_arms = Vec::new();
4244    let mut error_codecs = Vec::new();
4245    let mut wire_codecs = Vec::new();
4246
4247    for operation in &contract.operations {
4248        if operation.interaction != "request" {
4249            return Err(CodegenError::UnsupportedInteraction {
4250                operation: operation.name.clone(),
4251                interaction: operation.interaction.clone(),
4252            });
4253        }
4254        let operation_name = pascal_case(&operation.name);
4255        let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
4256        let response_type =
4257            types.type_for(&operation.response, &format!("{operation_name}Response"));
4258        let error_name = format!("{operation_name}Error");
4259        let known_errors = &operation.domain_errors;
4260        let error_definition = if known_errors.is_empty() {
4261            format!(
4262                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n    Unknown(UnknownDomainError),\n}}\n"
4263            )
4264        } else {
4265            let variants = known_errors
4266                .iter()
4267                .map(|variant| {
4268                    if let Some(payload) = &variant.payload {
4269                        let payload_name = format!("{error_name}{}Payload", variant.name);
4270                        let payload_type =
4271                            types.type_for_non_null(payload.non_null(), &payload_name);
4272                        let payload_type = if variant.payload_required {
4273                            if payload.is_nullable() {
4274                                format!("Option<{payload_type}>")
4275                            } else {
4276                                payload_type
4277                            }
4278                        } else if payload.is_nullable() {
4279                            format!("OptionalValue<{payload_type}>")
4280                        } else {
4281                            format!("Option<{payload_type}>")
4282                        };
4283                        format!("    {} {{ payload: {payload_type} }},", variant.name)
4284                    } else {
4285                        format!("    {},", variant.name)
4286                    }
4287                })
4288                .chain(std::iter::once(
4289                    "    Unknown(UnknownDomainError),".to_owned(),
4290                ))
4291                .collect::<Vec<_>>();
4292            format!(
4293                "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
4294                variants.join("\n")
4295            )
4296        };
4297        types.declarations.push(error_definition);
4298        error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
4299        wire_codecs.push(generate_rust_wire_codecs(
4300            &operation.name,
4301            &request_type,
4302            &response_type,
4303            &error_name,
4304        ));
4305        let method = rust_field_name(&operation.name);
4306        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4307        methods.push(format!(
4308            "    pub fn {method}(\n        &self,\n        context: &lenso_plugin_sdk::Ctx,\n        request: {request_type},\n    ) -> Result<{response_type}, lenso_plugin_sdk::CallError<{error_name}>> {{\n        context.request(&self.dependency, {operation_const}, &request)\n    }}"
4309        ));
4310        provider_methods.push(format!(
4311            "    fn {method}(\n        &self,\n        context: lenso_plugin_sdk::Ctx,\n        request: {request_type},\n    ) -> Result<{response_type}, {error_name}>;"
4312        ));
4313        dispatch_arms.push(format!(
4314            "                    $crate::{operation_const} => {{\n                        let request = match lenso_plugin_sdk::__private::serde_json::from_value::<$crate::{request_type}>(request) {{\n                            Ok(request) => request,\n                            Err(_) => return lenso_plugin_sdk::InvocationOutcome::Failure(\"invalid Capability request\".to_owned()),\n                        }};\n                        match <$plugin as $crate::{capability_name}Provider>::{method}(self, context, request) {{\n                            Ok(value) => match lenso_plugin_sdk::__private::serde_json::to_value(value) {{\n                                Ok(value) => lenso_plugin_sdk::InvocationOutcome::Success(value),\n                                Err(error) => lenso_plugin_sdk::InvocationOutcome::Failure(error.to_string()),\n                            }},\n                            Err(error) => match lenso_plugin_sdk::__private::serde_json::to_value(error) {{\n                                Ok(error) => lenso_plugin_sdk::InvocationOutcome::DomainError(error),\n                                Err(error) => lenso_plugin_sdk::InvocationOutcome::Failure(error.to_string()),\n                            }},\n                        }}\n                    }}"
4315        ));
4316    }
4317
4318    let mut output = String::new();
4319    output.push_str(GENERATED_HEADER);
4320    writeln!(
4321        output,
4322        "pub const CAPABILITY_ID: &str = {};",
4323        quote_string(&contract.capability_id)
4324    )
4325    .expect("writing to a String cannot fail");
4326    writeln!(
4327        output,
4328        "pub const DESCRIPTOR_VERSION: &str = {};",
4329        quote_string(&contract.version)
4330    )
4331    .expect("writing to a String cannot fail");
4332    writeln!(
4333        output,
4334        "pub const DESCRIPTOR_DIGEST: &str = {};",
4335        quote_string(&contract.descriptor_digest)
4336    )
4337    .expect("writing to a String cannot fail");
4338    writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
4339        .expect("writing to a String cannot fail");
4340    writeln!(
4341        output,
4342        "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;\npub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\npub const {capability_const}_DESCRIPTOR_DIGEST: &str = DESCRIPTOR_DIGEST;\n"
4343    )
4344    .expect("writing to a String cannot fail");
4345    for operation in &contract.operations {
4346        writeln!(
4347            output,
4348            "pub const {}_OPERATION: &str = {};",
4349            screaming_snake_case(&operation.name),
4350            quote_string(&operation.name)
4351        )
4352        .expect("writing to a String cannot fail");
4353    }
4354    writeln!(
4355        output,
4356        "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
4357        rust_runtime_types(contract).join(", ")
4358    )
4359    .expect("writing to a String cannot fail");
4360    for declaration in types.declarations {
4361        output.push_str(&declaration);
4362        output.push('\n');
4363    }
4364    for codec in error_codecs {
4365        output.push_str(&codec);
4366        output.push('\n');
4367    }
4368    for codec in wire_codecs {
4369        output.push_str(&codec);
4370        output.push('\n');
4371    }
4372    write!(
4373        output,
4374        "#[derive(Clone, Debug)]\npub struct {capability_name}Client {{\n    dependency: lenso_plugin_sdk::Dependency,\n}}\n\nimpl lenso_plugin_sdk::DependencyClient for {capability_name}Client {{\n    const CAPABILITY_ID: &'static str = CAPABILITY_ID;\n    const DESCRIPTOR_VERSION: &'static str = DESCRIPTOR_VERSION;\n    const DESCRIPTOR_DIGEST: &'static str = DESCRIPTOR_DIGEST;\n\n    fn from_dependency(dependency: lenso_plugin_sdk::Dependency) -> Self {{\n        Self {{ dependency }}\n    }}\n}}\n\nimpl {capability_name}Client {{\n{}\n}}\n",
4375        methods.join("\n\n")
4376    )
4377    .expect("writing to a String cannot fail");
4378    let export_macro = format!("export_{}_plugin", snake_case(&capability_name));
4379    let request_literals = contract
4380        .operations
4381        .iter()
4382        .map(|operation| quote_string(&operation.name))
4383        .collect::<Vec<_>>()
4384        .join(", ");
4385    write!(
4386        output,
4387        "\npub trait {capability_name}Provider {{\n{}\n}}\n\n#[macro_export]\nmacro_rules! {export_macro} {{\n    ($plugin:ty) => {{\n        impl lenso_plugin_sdk::JsonRequestHandler for $plugin {{\n            fn invoke_with_context(\n                &self,\n                context: lenso_plugin_sdk::Ctx,\n                capability: &str,\n                operation: &str,\n                request: lenso_plugin_sdk::__private::serde_json::Value,\n            ) -> lenso_plugin_sdk::InvocationOutcome {{\n                if capability != $crate::CAPABILITY_ID {{\n                    return lenso_plugin_sdk::InvocationOutcome::Failure(\"unknown Capability\".to_owned());\n                }}\n                match operation {{\n{}\n                    _ => lenso_plugin_sdk::InvocationOutcome::Failure(\"unknown Capability operation\".to_owned()),\n                }}\n            }}\n        }}\n\n        lenso_plugin_sdk::__export_json_request_handler! {{\n            $plugin {{\n                capability_id: {},\n                descriptor_version: {},\n                descriptor_digest: {},\n                requests: [{request_literals}],\n            }}\n        }}\n    }};\n}}\n",
4388        provider_methods.join("\n\n"),
4389        dispatch_arms.join(",\n"),
4390        quote_string(&contract.capability_id),
4391        quote_string(&contract.version),
4392        quote_string(&contract.descriptor_digest),
4393    )
4394    .expect("writing to a String cannot fail");
4395    Ok(format!("{}\n", output.trim_end()))
4396}
4397
4398fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
4399    let mut types = BTreeSet::from(["UnknownDomainError"]);
4400    for operation in &contract.operations {
4401        collect_rust_runtime_types(&operation.request, &mut types);
4402        collect_rust_runtime_types(&operation.response, &mut types);
4403        for error in &operation.domain_errors {
4404            if let Some(payload) = &error.payload {
4405                if !error.payload_required && payload.is_nullable() {
4406                    types.insert("OptionalValue");
4407                }
4408                collect_rust_runtime_types(payload, &mut types);
4409            }
4410        }
4411    }
4412    types.into_iter().collect()
4413}
4414
4415fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
4416    match ty {
4417        TypeIr::Int64 => {
4418            types.insert("Int64");
4419        }
4420        TypeIr::Uint64 => {
4421            types.insert("Uint64");
4422        }
4423        TypeIr::Bytes => {
4424            types.insert("Bytes");
4425        }
4426        TypeIr::Timestamp => {
4427            types.insert("Timestamp");
4428        }
4429        TypeIr::Duration => {
4430            types.insert("Duration");
4431        }
4432        TypeIr::RawJson => {
4433            types.insert("RawJson");
4434        }
4435        TypeIr::Array(item) | TypeIr::Nullable(item) => {
4436            collect_rust_runtime_types(item, types);
4437        }
4438        TypeIr::Object {
4439            fields, additional, ..
4440        } => {
4441            for field in fields {
4442                if !field.required && field.ty.is_nullable() {
4443                    types.insert("OptionalValue");
4444                }
4445                collect_rust_runtime_types(&field.ty, types);
4446            }
4447            if let ObjectAdditionalIr::Typed(value) = additional {
4448                collect_rust_runtime_types(value, types);
4449            }
4450        }
4451        TypeIr::Any
4452        | TypeIr::String
4453        | TypeIr::Integer
4454        | TypeIr::Number
4455        | TypeIr::Boolean
4456        | TypeIr::Null
4457        | TypeIr::Enum { .. } => {}
4458    }
4459}
4460
4461#[allow(clippy::too_many_lines)]
4462fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
4463    let mut output = String::new();
4464    writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
4465    output.push_str(
4466        "    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n",
4467    );
4468    output.push_str("        use serde::ser::SerializeMap;\n");
4469    output.push_str("        match self {\n");
4470    for variant in variants {
4471        if variant.structured {
4472            if variant.payload.is_some() {
4473                if variant.payload_required {
4474                    writeln!(
4475                        output,
4476                        "            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            }},",
4477                        variant.name,
4478                        quote_string(&variant.code)
4479                    )
4480                    .expect("String cannot fail");
4481                } else {
4482                    writeln!(
4483                        output,
4484                        "            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            }},",
4485                        variant.name,
4486                        quote_string(&variant.code)
4487                    )
4488                    .expect("String cannot fail");
4489                }
4490            } else {
4491                writeln!(
4492                    output,
4493                    "            Self::{} => {{\n                let mut map = serializer.serialize_map(Some(1))?;\n                map.serialize_entry(\"code\", {})?;\n                map.end()\n            }},",
4494                    variant.name,
4495                    quote_string(&variant.code)
4496                )
4497                .expect("String cannot fail");
4498            }
4499        } else {
4500            writeln!(
4501                output,
4502                "            Self::{} => serializer.serialize_str({}),",
4503                variant.name,
4504                quote_string(&variant.code)
4505            )
4506            .expect("String cannot fail");
4507        }
4508    }
4509    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");
4510    writeln!(
4511        output,
4512        "impl<'de> serde::Deserialize<'de> for {error_name} {{"
4513    )
4514    .expect("String cannot fail");
4515    output.push_str(
4516        "    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",
4517    );
4518    for variant in variants.iter().filter(|variant| !variant.structured) {
4519        writeln!(
4520            output,
4521            "                {} => Ok(Self::{}),",
4522            quote_string(&variant.code),
4523            variant.name
4524        )
4525        .expect("String cannot fail");
4526    }
4527    output.push_str(
4528        "                _ => 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",
4529    );
4530    if variants.iter().any(|variant| variant.structured) {
4531        output.push_str("                match code.as_str() {\n");
4532        for variant in variants.iter().filter(|variant| variant.structured) {
4533            if variant.payload.is_some() {
4534                if variant.payload_required {
4535                    writeln!(
4536                        output,
4537                        "                    {} => {{\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                    }},",
4538                        quote_string(&variant.code),
4539                        variant.name
4540                    )
4541                    .expect("String cannot fail");
4542                } else {
4543                    writeln!(
4544                        output,
4545                        "                    {} => {{\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                    }},",
4546                        quote_string(&variant.code),
4547                        variant.name
4548                    )
4549                    .expect("String cannot fail");
4550                }
4551            } else {
4552                writeln!(
4553                    output,
4554                    "                    {} => Ok(Self::{}),",
4555                    quote_string(&variant.code),
4556                    variant.name
4557                )
4558                .expect("String cannot fail");
4559            }
4560        }
4561        output.push_str(
4562            "                    _ => {\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",
4563        );
4564    } else {
4565        output.push_str(
4566            "                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",
4567        );
4568    }
4569    output.push_str(
4570        "            other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n        }\n    }\n}\n",
4571    );
4572    output
4573}
4574
4575#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
4576fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
4577    let capability_name = pascal_case(
4578        contract
4579            .capability_id
4580            .split('@')
4581            .next()
4582            .and_then(|identity| identity.rsplit('.').next())
4583            .unwrap_or("Capability"),
4584    );
4585    let codec_name = format!("{capability_name}JsonCodec");
4586    let guest_client_name = format!("{capability_name}GuestClient");
4587    let mut types = RustTypes::new();
4588    let mut request_operations = Vec::new();
4589    let mut stream_operations = Vec::new();
4590    let mut event_operations = Vec::new();
4591    let mut encode_arms = Vec::new();
4592    let mut response_arms = Vec::new();
4593    let mut error_arms = Vec::new();
4594    let mut host_request_arms = Vec::new();
4595    let mut stream_open_arms = Vec::new();
4596    let mut stream_encode_arms = Vec::new();
4597    let mut stream_decode_arms = Vec::new();
4598    let mut stream_error_arms = Vec::new();
4599    let mut host_stream_arms = Vec::new();
4600    let mut event_encode_arms = Vec::new();
4601    let mut host_event_arms = Vec::new();
4602    let mut guest_methods = Vec::new();
4603
4604    for operation in &contract.operations {
4605        let operation_name = pascal_case(&operation.name);
4606        let marker_name = if contract.operations.len() == 1 {
4607            capability_name.clone()
4608        } else {
4609            format!("{capability_name}{operation_name}")
4610        };
4611        let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
4612        let response_type =
4613            types.type_for(&operation.response, &format!("{operation_name}Response"));
4614        let error_type = format!("{operation_name}Error");
4615        let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4616        let method_name = rust_field_name(&operation.name);
4617        match operation.interaction.as_str() {
4618            "request" => {
4619                request_operations.push(operation_const.clone());
4620                encode_arms.push(format!(
4621                    "            {operation_const} => {{\n                let value = request.downcast_ref::<{request_type}>().ok_or_else(runtime_codec_protocol_failure)?;\n                serde_json::to_value(value).map_err(|_| runtime_codec_protocol_failure())\n            }}"
4622                ));
4623                response_arms.push(format!(
4624                    "            {operation_const} => serde_json::from_value::<{response_type}>(value)\n                .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                .map_err(|_| runtime_codec_protocol_failure()),"
4625                ));
4626                error_arms.push(format!(
4627                    "            {operation_const} => serde_json::from_value::<{error_type}>(value)\n                .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                .map_err(|_| runtime_codec_protocol_failure()),"
4628                ));
4629                host_request_arms.push(format!(
4630                    "            {operation_const} => {{\n                let request = serde_json::from_value::<{request_type}>(request).map_err(|_| runtime_codec_protocol_failure());\n                Box::pin(async move {{\n                    let request = request?;\n                    let handle = dependency.typed::<{marker_name}>()?;\n                    match handle.invoke_with_context({operation_const}, context, request).await? {{\n                        Ok(response) => serde_json::to_value(response)\n                            .map(lenso_runtime_codec::JsonInvocationOutcome::Success)\n                            .map_err(|_| runtime_codec_protocol_failure()),\n                        Err(error) => serde_json::to_value(error)\n                            .map(lenso_runtime_codec::JsonInvocationOutcome::DomainError)\n                            .map_err(|_| runtime_codec_protocol_failure()),\n                    }}\n                }})\n            }}"
4631                ));
4632                guest_methods.push(format!(
4633                    "    pub fn {method_name}(&self, request: &{request_type}) -> Result<{response_type}, lenso_guest_sdk::GuestError<{error_type}>> {{\n        self.capability.request({operation_const}, request)\n    }}"
4634                ));
4635            }
4636            "stream" => {
4637                stream_operations.push(operation_const.clone());
4638                stream_open_arms.push(format!(
4639                    "            {operation_const} => {{\n                let value = request.downcast_ref::<{request_type}>().ok_or_else(runtime_codec_protocol_failure)?;\n                serde_json::to_value(value).map_err(|_| runtime_codec_protocol_failure())\n            }}"
4640                ));
4641                stream_encode_arms.push(format!(
4642                    "            {operation_const} => {{\n                let value = message.downcast_ref::<{response_type}>().ok_or_else(runtime_codec_protocol_failure)?;\n                serde_json::to_value(value).map_err(|_| runtime_codec_protocol_failure())\n            }}"
4643                ));
4644                stream_decode_arms.push(format!(
4645                    "            {operation_const} => serde_json::from_value::<{response_type}>(value)\n                .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                .map_err(|_| runtime_codec_protocol_failure()),"
4646                ));
4647                stream_error_arms.push(format!(
4648                    "            {operation_const} => serde_json::from_value::<{error_type}>(value)\n                .map(|value| Box::new(value) as Box<dyn std::any::Any>)\n                .map_err(|_| runtime_codec_protocol_failure()),"
4649                ));
4650                host_stream_arms.push(format!(
4651                    "            {operation_const} => {{\n                let request = serde_json::from_value::<{request_type}>(request).map_err(|_| runtime_codec_protocol_failure());\n                Box::pin(async move {{\n                    let request = request?;\n                    let handle = dependency.typed::<{marker_name}>()?;\n                    match handle.open_with_context({operation_const}, context, request).await? {{\n                        Ok(stream) => Ok(Ok(lenso_runtime_codec::json_host_stream::<{marker_name}>(\n                            stream,\n                            |value| serde_json::from_value::<{response_type}>(value).map_err(|_| runtime_codec_protocol_failure()),\n                            |message| serde_json::to_value(message).map_err(|_| runtime_codec_protocol_failure()),\n                            |error| serde_json::to_value(error).map_err(|_| runtime_codec_protocol_failure()),\n                        ))),\n                        Err(error) => serde_json::to_value(error)\n                            .map(Err)\n                            .map_err(|_| runtime_codec_protocol_failure()),\n                    }}\n                }})\n            }}"
4652                ));
4653                guest_methods.push(format!(
4654                    "    pub fn {method_name}(&self, request: &{request_type}) -> Result<lenso_guest_sdk::GuestStream<H, {response_type}, {error_type}>, lenso_guest_sdk::GuestError<{error_type}>> {{\n        self.capability.open_stream({operation_const}, request)\n    }}"
4655                ));
4656            }
4657            "event" => {
4658                event_operations.push(operation_const.clone());
4659                event_encode_arms.push(format!(
4660                    "            {operation_const} => {{\n                let value = event.downcast_ref::<{request_type}>().ok_or_else(runtime_codec_protocol_failure)?;\n                serde_json::to_value(value).map_err(|_| runtime_codec_protocol_failure())\n            }}"
4661                ));
4662                host_event_arms.push(format!(
4663                    "            {operation_const} => {{\n                let event = serde_json::from_value::<{request_type}>(event).map_err(|_| runtime_codec_protocol_failure());\n                Box::pin(async move {{\n                    let event = event?;\n                    let handle = dependency.typed::<{marker_name}>()?;\n                    let results = handle.publish_with_context({operation_const}, context, event).await;\n                    let [result] = results.as_slice() else {{ return Err(runtime_codec_protocol_failure()); }};\n                    match result.admission() {{\n                        lenso_kernel::EventAdmission::Accepted => Ok(()),\n                        lenso_kernel::EventAdmission::Unavailable => Err(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }}),\n                        lenso_kernel::EventAdmission::Exhausted => Err(RuntimeFailure::ResourceExhausted {{ capability: CAPABILITY_ID, operation: {operation_const}.to_owned() }}),\n                    }}\n                }})\n            }}"
4664                ));
4665                guest_methods.push(format!(
4666                    "    pub fn {method_name}(&self, event: &{request_type}) -> Result<(), lenso_guest_sdk::GuestError<serde_json::Value>> {{\n        self.capability.publish_event({operation_const}, event)\n    }}"
4667                ));
4668            }
4669            _ => unreachable!("Descriptor validation restricts interactions"),
4670        }
4671    }
4672
4673    let encode_dispatch = runtime_codec_dispatch(&encode_arms);
4674    let response_dispatch = runtime_codec_dispatch(&response_arms);
4675    let error_dispatch = runtime_codec_dispatch(&error_arms);
4676    let host_request_dispatch = runtime_codec_host_dispatch(&host_request_arms);
4677    let stream_open_dispatch = runtime_codec_dispatch(&stream_open_arms);
4678    let stream_encode_dispatch = runtime_codec_dispatch(&stream_encode_arms);
4679    let stream_decode_dispatch = runtime_codec_dispatch(&stream_decode_arms);
4680    let stream_error_dispatch = runtime_codec_dispatch(&stream_error_arms);
4681    let host_stream_dispatch = runtime_codec_host_dispatch(&host_stream_arms);
4682    let event_encode_dispatch = runtime_codec_dispatch(&event_encode_arms);
4683    let host_event_dispatch = runtime_codec_host_dispatch(&host_event_arms);
4684    let request_parameter = if request_operations.is_empty() {
4685        "_request"
4686    } else {
4687        "request"
4688    };
4689    let request_value_parameter = if request_operations.is_empty() {
4690        "_value"
4691    } else {
4692        "value"
4693    };
4694    let stream_request_parameter = if stream_operations.is_empty() {
4695        "_request"
4696    } else {
4697        "request"
4698    };
4699    let stream_message_parameter = if stream_operations.is_empty() {
4700        "_message"
4701    } else {
4702        "message"
4703    };
4704    let stream_value_parameter = if stream_operations.is_empty() {
4705        "_value"
4706    } else {
4707        "value"
4708    };
4709    let event_parameter = if event_operations.is_empty() {
4710        "_event"
4711    } else {
4712        "event"
4713    };
4714    let (host_request_dependency_parameter, host_request_parameter, host_request_context_parameter) =
4715        if request_operations.is_empty() {
4716            ("_dependency", "_request", "_context")
4717        } else {
4718            ("dependency", "request", "context")
4719        };
4720    let (host_stream_dependency_parameter, host_stream_parameter, host_stream_context_parameter) =
4721        if stream_operations.is_empty() {
4722            ("_dependency", "_request", "_context")
4723        } else {
4724            ("dependency", "request", "context")
4725        };
4726    let (host_event_dependency_parameter, host_event_parameter, host_event_context_parameter) =
4727        if event_operations.is_empty() {
4728            ("_dependency", "_event", "_context")
4729        } else {
4730            ("dependency", "event", "context")
4731        };
4732
4733    let mut output = generate_rust(contract);
4734    write!(
4735        output,
4736        "\n#[derive(Clone, Copy, Debug)]\npub struct {guest_client_name}<'a, H: lenso_guest_sdk::HostImports> {{\n    capability: lenso_guest_sdk::GuestCapability<'a, H>,\n}}\n\nimpl<'a, H: lenso_guest_sdk::HostImports> {guest_client_name}<'a, H> {{\n    pub fn from_context(context: &'a lenso_guest_sdk::GuestContext<H>) -> Result<Self, lenso_guest_sdk::GuestError<serde_json::Value>> {{\n        context\n            .require(CAPABILITY_ID, DESCRIPTOR_VERSION, &[{}], &[{}], &[{}])\n            .map(|capability| Self {{ capability }})\n    }}\n\n{}\n}}\n",
4737        request_operations.join(", "),
4738        stream_operations.join(", "),
4739        event_operations.join(", "),
4740        guest_methods.join("\n\n"),
4741    )
4742    .expect("writing generated Rust to a String cannot fail");
4743    write!(
4744        output,
4745        "\n#[derive(Debug, Default)]\npub struct {codec_name};\n\nimpl lenso_runtime_codec::JsonCapabilityCodec for {codec_name} {{\n    fn capability_id(&self) -> &'static str {{ CAPABILITY_ID }}\n\n    fn descriptor_version(&self) -> &'static str {{ DESCRIPTOR_VERSION }}\n\n    fn descriptor_digest(&self) -> &'static str {{ DESCRIPTOR_DIGEST }}\n\n    fn request_operations(&self) -> &'static [&'static str] {{ &[{}] }}\n    fn stream_operations(&self) -> &'static [&'static str] {{ &[{}] }}\n    fn event_operations(&self) -> &'static [&'static str] {{ &[{}] }}\n\n    fn encode_request(&self, operation: &str, {request_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{encode_dispatch}\n    }}\n\n    fn decode_response(&self, operation: &str, {request_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{response_dispatch}\n    }}\n\n    fn decode_domain_error(&self, operation: &str, {request_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{error_dispatch}\n    }}\n\n    fn encode_stream_open(&self, operation: &str, {stream_request_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{stream_open_dispatch}\n    }}\n\n    fn encode_stream_message(&self, operation: &str, {stream_message_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{stream_encode_dispatch}\n    }}\n\n    fn decode_stream_message(&self, operation: &str, {stream_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{stream_decode_dispatch}\n    }}\n\n    fn decode_stream_domain_error(&self, operation: &str, {stream_value_parameter}: serde_json::Value) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n{stream_error_dispatch}\n    }}\n\n    fn encode_event(&self, operation: &str, {event_parameter}: &dyn std::any::Any) -> Result<serde_json::Value, RuntimeFailure> {{\n{event_encode_dispatch}\n    }}\n\n    fn invoke_host_request(&self, {host_request_dependency_parameter}: lenso_kernel::PluginDependencyHandle, operation: String, {host_request_parameter}: serde_json::Value, {host_request_context_parameter}: InvocationContext) -> lenso_runtime_codec::JsonHostRequestFuture {{\n{host_request_dispatch}\n    }}\n\n    fn open_host_stream(&self, {host_stream_dependency_parameter}: lenso_kernel::PluginStreamDependencyHandle, operation: String, {host_stream_parameter}: serde_json::Value, {host_stream_context_parameter}: InvocationContext) -> lenso_runtime_codec::JsonHostStreamOpenFuture {{\n{host_stream_dispatch}\n    }}\n\n    fn publish_host_event(&self, {host_event_dependency_parameter}: lenso_kernel::PluginEventDependencyHandle, operation: String, {host_event_parameter}: serde_json::Value, {host_event_context_parameter}: InvocationContext) -> futures::future::LocalBoxFuture<'static, Result<(), RuntimeFailure>> {{\n{host_event_dispatch}\n    }}\n}}\n\nfn runtime_codec_protocol_failure() -> RuntimeFailure {{ RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }} }}\n\nfn runtime_codec_unknown_operation(operation: &str) -> RuntimeFailure {{\n    RuntimeFailure::UnknownOperation {{ capability: CAPABILITY_ID, operation: operation.to_owned() }}\n}}\n",
4746        request_operations.join(", "),
4747        stream_operations.join(", "),
4748        event_operations.join(", "),
4749    )
4750    .expect("writing generated Rust to a String cannot fail");
4751    Ok(output)
4752}
4753
4754fn runtime_codec_host_dispatch(arms: &[String]) -> String {
4755    if arms.is_empty() {
4756        "        Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation))))"
4757            .to_owned()
4758    } else {
4759        let arms = arms
4760            .iter()
4761            .map(|arm| arm.trim_end_matches(','))
4762            .collect::<Vec<_>>()
4763            .join(",\n");
4764        format!(
4765            "        match operation.as_str() {{\n{arms},\n            _ => Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation)))),\n        }}"
4766        )
4767    }
4768}
4769
4770fn runtime_codec_dispatch(arms: &[String]) -> String {
4771    if arms.is_empty() {
4772        "        Err(runtime_codec_unknown_operation(operation))".to_owned()
4773    } else {
4774        let arms = arms
4775            .iter()
4776            .map(|arm| arm.trim_end_matches(','))
4777            .collect::<Vec<_>>()
4778            .join(",\n");
4779        format!(
4780            "        match operation {{\n{arms},\n            _ => Err(runtime_codec_unknown_operation(operation)),\n        }}"
4781        )
4782    }
4783}
4784
4785fn generate_rust_wire_codecs(
4786    operation: &str,
4787    request_type: &str,
4788    response_type: &str,
4789    error_type: &str,
4790) -> String {
4791    let stem = snake_case(operation);
4792    format!(
4793        "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"
4794    )
4795}
4796
4797fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
4798    let stem = snake_case(operation);
4799    format!(
4800        "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"
4801    )
4802}
4803
4804fn generate_typescript_codecs(
4805    operation: &str,
4806    request_type: &str,
4807    response_type: &str,
4808    error_type: &str,
4809    variants: &[ErrorVariantIr],
4810) -> String {
4811    let stem = pascal_case(operation);
4812    let known_strings = variants
4813        .iter()
4814        .filter(|variant| !variant.structured)
4815        .map(|variant| quote_string(&variant.code))
4816        .collect::<Vec<_>>()
4817        .join(", ");
4818    format!(
4819        "export function encode{stem}Request(value: {request_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"request\"); }}\nexport function decode{stem}Request(wire: string): {request_type} {{ return lensoContractRuntime.decodePortableJson<{request_type}>(wire); }}\nexport function encode{stem}Response(value: {response_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"response\"); }}\nexport function decode{stem}Response(wire: string): {response_type} {{ return lensoContractRuntime.decodePortableJson<{response_type}>(wire); }}\nexport function encode{stem}Error(value: {error_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"Domain Error\"); }}\nexport function decode{stem}Error(wire: string): {error_type} {{ return lensoContractRuntime.decodeDomainError<{error_type}>(wire, [{known_strings}]); }}\n"
4820    )
4821}
4822
4823fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
4824    let stem = pascal_case(operation);
4825    format!(
4826        "export function encode{stem}Event(value: {event_type}): string {{ return lensoContractRuntime.encodePortableJson(value, \"event\"); }}\nexport function decode{stem}Event(wire: string): {event_type} {{ return lensoContractRuntime.decodePortableJson<{event_type}>(wire); }}\n"
4827    )
4828}
4829
4830#[allow(clippy::too_many_lines)]
4831fn generate_typescript(contract: &ContractIr) -> String {
4832    let capability_name = pascal_case(
4833        contract
4834            .capability_id
4835            .split('@')
4836            .next()
4837            .and_then(|identity| identity.rsplit('.').next())
4838            .unwrap_or("Capability"),
4839    );
4840    let capability_const = screaming_snake_case(&capability_name);
4841    let mut types = TypeScriptTypes::new();
4842    let mut clients = Vec::new();
4843    let mut providers = Vec::new();
4844    let mut errors = Vec::new();
4845    let mut codecs = Vec::new();
4846    let mut request_dispatch_arms = Vec::new();
4847    let mut stream_dispatch_arms = Vec::new();
4848    let mut event_dispatch_arms = Vec::new();
4849    let mut dependency_methods = Vec::new();
4850    let operation_names = contract
4851        .operations
4852        .iter()
4853        .map(|operation| quote_string(&operation.name))
4854        .collect::<Vec<_>>()
4855        .join(", ");
4856    let stream_operation_names = contract
4857        .operations
4858        .iter()
4859        .filter(|operation| operation.interaction == "stream")
4860        .map(|operation| quote_string(&operation.name))
4861        .collect::<Vec<_>>()
4862        .join(", ");
4863    let event_operation_names = contract
4864        .operations
4865        .iter()
4866        .filter(|operation| operation.interaction == "event")
4867        .map(|operation| quote_string(&operation.name))
4868        .collect::<Vec<_>>()
4869        .join(", ");
4870    let has_stream_operations = contract
4871        .operations
4872        .iter()
4873        .any(|operation| operation.interaction == "stream");
4874    let has_event_operations = contract
4875        .operations
4876        .iter()
4877        .any(|operation| operation.interaction == "event");
4878    let dependency_invoker_type = if has_stream_operations || has_event_operations {
4879        "InteractionDependencyInvoker"
4880    } else {
4881        "DependencyInvoker"
4882    };
4883    for operation in &contract.operations {
4884        let operation_name = pascal_case(&operation.name);
4885        let request_name = format!("{operation_name}Request");
4886        let response_name = format!("{operation_name}Response");
4887        let error_name = format!("{operation_name}Error");
4888        let request_type = types.type_for(&operation.request, &request_name);
4889        let response_type = types.type_for(&operation.response, &response_name);
4890        let variants = &operation.domain_errors;
4891        let error_type = if variants.is_empty() {
4892            "UnknownDomainError".to_owned()
4893        } else {
4894            let mut values = variants
4895                .iter()
4896                .map(|variant| {
4897                    if let Some(payload) = &variant.payload {
4898                        let payload_name = format!("{error_name}{}Payload", variant.name);
4899                        let payload_type =
4900                            types.type_for_non_null(payload.non_null(), &payload_name);
4901                        let payload_type = if payload.is_nullable() {
4902                            format!("{payload_type} | null")
4903                        } else {
4904                            payload_type
4905                        };
4906                        let payload_optional = if variant.payload_required { "" } else { "?" };
4907                        format!(
4908                            "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
4909                            quote_string(&variant.code),
4910                        )
4911                    } else if variant.structured {
4912                        format!("{{ readonly code: {} }}", quote_string(&variant.code))
4913                    } else {
4914                        quote_string(&variant.code)
4915                    }
4916                })
4917                .collect::<Vec<_>>();
4918            values.push("UnknownDomainError".to_owned());
4919            values.join(" | ")
4920        };
4921        let invocation_error_name = format!("{operation_name}InvocationError");
4922        let result_name = format!("{operation_name}Result");
4923        let provider_result_name = if operation.interaction == "stream" {
4924            format!("{operation_name}ProviderResult")
4925        } else {
4926            result_name.clone()
4927        };
4928        let provider_return_type = if operation.interaction == "stream" {
4929            format!("{operation_name}ProviderOutput")
4930        } else {
4931            format!("Promise<{provider_result_name}>")
4932        };
4933        let result_value_type = match operation.interaction.as_str() {
4934            "stream" => format!("StreamSession<{response_type}, {error_name}>"),
4935            "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
4936            _ => response_type.clone(),
4937        };
4938        if operation.interaction == "event" {
4939            errors.push(format!(
4940                "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
4941            ));
4942        } else if operation.interaction == "stream" {
4943            errors.push(format!(
4944                "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} }};\nexport type {provider_result_name} = {{ readonly ok: true; readonly value: ProviderStream<{response_type}, {error_name}> }} | {{ readonly ok: false; readonly error: {invocation_error_name} }};\nexport type {operation_name}ProviderOutput = AsyncIterable<{response_type}> | {provider_result_name} | Promise<{provider_result_name}>;"
4945            ));
4946        } else {
4947            errors.push(format!(
4948                "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} }};"
4949            ));
4950        }
4951        codecs.push(generate_typescript_codecs(
4952            &operation.name,
4953            &request_type,
4954            &response_type,
4955            &error_name,
4956            variants,
4957        ));
4958        if operation.interaction == "event" {
4959            codecs.push(generate_typescript_event_codecs(
4960                &operation.name,
4961                &request_type,
4962            ));
4963        }
4964        if matches!(operation.interaction.as_str(), "request" | "stream") {
4965            clients.push(format!(
4966                "  {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4967                typescript_property_name(&snake_case(&operation.name)),
4968            ));
4969            providers.push(format!(
4970                "  {}(context: InvocationContext, request: {request_type}): {provider_return_type};",
4971                typescript_property_name(&snake_case(&operation.name)),
4972            ));
4973        } else {
4974            clients.push(format!(
4975                "  {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4976                typescript_property_name(&snake_case(&operation.name)),
4977            ));
4978            providers.push(format!(
4979                "  {}(context: InvocationContext, event: {request_type}): void | Promise<void>;",
4980                typescript_property_name(&snake_case(&operation.name)),
4981            ));
4982        }
4983        if operation.interaction == "request" {
4984            let provider_method = typescript_property_name(&snake_case(&operation.name));
4985            request_dispatch_arms.push(format!(
4986                "      case {}: {{\n        let request: {request_type};\n        try {{\n          request = decode{operation_name}Request(lensoContractRuntime.encodePortableJson(payload, \"request\"));\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"protocol_violation\", detail: providerErrorMessage(error) }} }};\n        }}\n        try {{\n          const result = await provider.{provider_method}(context, request);\n          if (result.ok) {{\n            return {{ kind: \"success\", value: JSON.parse(encode{operation_name}Response(result.value)) as unknown }};\n          }}\n          if (result.error.kind === \"domain\") {{\n            return {{ kind: \"domain\", value: JSON.parse(encode{operation_name}Error(result.error.error)) as unknown }};\n          }}\n          return {{ kind: \"runtime\", failure: result.error.error }};\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n        }}\n      }}",
4987                quote_string(&operation.name),
4988            ));
4989            dependency_methods.push(format!(
4990                "      async {provider_method}(request, context) {{\n        let payload: unknown;\n        try {{\n          payload = JSON.parse(encode{operation_name}Request(request)) as unknown;\n        }} catch (error) {{\n          return {{ ok: false, error: {{ kind: \"runtime\", error: {{ kind: \"protocol_violation\", detail: dependencyErrorMessage(error) }} }} }};\n        }}\n        const call = context ?? {{ requestId: \"0\" as Uint64, cancelled: false }};\n        try {{\n          const outcome = await invoke({}, call, payload);\n          if (outcome.kind === \"success\") {{\n            return {{ ok: true, value: decode{operation_name}Response(JSON.stringify(outcome.value)) }};\n          }}\n          if (outcome.kind === \"domain\") {{\n            return {{ ok: false, error: {{ kind: \"domain\", error: decode{operation_name}Error(JSON.stringify(outcome.value)) }} }};\n          }}\n          return {{ ok: false, error: {{ kind: \"runtime\", error: outcome.failure }} }};\n        }} catch (error) {{\n          return {{ ok: false, error: {{ kind: \"runtime\", error: {{ kind: \"plugin_failure\", detail: dependencyErrorMessage(error) }} }} }};\n        }}\n      }},",
4991                quote_string(&operation.name),
4992            ));
4993        } else if operation.interaction == "stream" {
4994            let provider_method = typescript_property_name(&snake_case(&operation.name));
4995            stream_dispatch_arms.push(format!(
4996                "      case {}: {{\n        let request: {request_type};\n        try {{\n          request = decode{operation_name}Request(lensoContractRuntime.encodePortableJson(payload, \"stream open request\"));\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"protocol_violation\", detail: providerErrorMessage(error) }} }};\n        }}\n        try {{\n          const provided = provider.{provider_method}(context, request);\n          const result = isAsyncIterable<{response_type}>(provided)\n            ? {{ ok: true as const, value: provided }}\n            : await provided;\n          if (!result.ok) {{\n            if (result.error.kind === \"domain\") {{\n              return {{ kind: \"domain\", value: JSON.parse(encode{operation_name}Error(result.error.error)) as unknown }};\n            }}\n            return {{ kind: \"runtime\", failure: result.error.error }};\n          }}\n          const stream = lowerProviderStream(result.value);\n          const binding: ProviderStreamSessionBinding = {{\n            async send(message) {{\n              let decoded: {response_type};\n              try {{\n                decoded = decode{operation_name}Response(lensoContractRuntime.encodePortableJson(message, \"stream message\"));\n              }} catch (error) {{\n                return {{ kind: \"runtime\", failure: {{ kind: \"protocol_violation\", detail: providerErrorMessage(error) }} }};\n              }}\n              try {{\n                await stream.send(decoded);\n                return {{ kind: \"accepted\" }};\n              }} catch (error) {{\n                return {{ kind: \"runtime\", failure: error instanceof ServerOutputStreamInputError\n                  ? {{ kind: \"protocol_violation\", detail: providerErrorMessage(error) }}\n                  : {{ kind: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n              }}\n            }},\n            async receive() {{\n              try {{\n                const event = await stream.receive();\n                if (event.kind === \"message\") {{\n                  return {{ kind: \"message\", value: JSON.parse(encode{operation_name}Response(event.message)) as unknown }};\n                }}\n                if (event.kind === \"peer_half_closed\") return {{ kind: \"peer_half_closed\" }};\n                if (event.outcome.ok) return {{ kind: \"terminal_success\" }};\n                return {{ kind: \"terminal_domain\", value: JSON.parse(encode{operation_name}Error(event.outcome.error)) as unknown }};\n              }} catch (error) {{\n                return {{ kind: \"runtime\", failure: {{ kind: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n              }}\n            }},\n            async closeSend() {{\n              try {{\n                await stream.closeSend();\n                return {{ kind: \"accepted\" }};\n              }} catch (error) {{\n                return {{ kind: \"runtime\", failure: {{ kind: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n              }}\n            }},\n            cancel() {{ stream.cancel(); }},\n          }};\n          return {{ kind: \"opened\", stream: binding }};\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n        }}\n      }}",
4997                quote_string(&operation.name),
4998            ));
4999            dependency_methods.push(format!(
5000                "      async {provider_method}(request, context) {{\n        let payload: unknown;\n        try {{\n          payload = JSON.parse(encode{operation_name}Request(request)) as unknown;\n        }} catch (error) {{\n          return {{ ok: false, error: {{ kind: \"runtime\", error: {{ kind: \"protocol_violation\", detail: dependencyErrorMessage(error) }} }} }};\n        }}\n        const call = context ?? {{ requestId: \"0\" as Uint64, cancelled: false }};\n        try {{\n          const outcome = await invoke.openStream({}, call, payload);\n          if (outcome.kind === \"domain\") {{\n            return {{ ok: false, error: {{ kind: \"domain\", error: decode{operation_name}Error(JSON.stringify(outcome.value)) }} }};\n          }}\n          if (outcome.kind === \"runtime\") {{\n            return {{ ok: false, error: {{ kind: \"runtime\", error: outcome.failure }} }};\n          }}\n          const stream = outcome.stream;\n          return {{ ok: true, value: {{\n            async send(message) {{\n              const encoded = JSON.parse(encode{operation_name}Response(message)) as unknown;\n              const result = await stream.send(encoded);\n              if (result.kind === \"runtime\") throw dependencyRuntimeError(result.failure);\n            }},\n            async receive() {{\n              const result = await stream.receive();\n              if (result.kind === \"message\") return {{ kind: \"message\", message: decode{operation_name}Response(JSON.stringify(result.value)) }};\n              if (result.kind === \"peer_half_closed\") return {{ kind: \"peer_half_closed\" }};\n              if (result.kind === \"terminal_success\") return {{ kind: \"terminal\", outcome: {{ ok: true }} }};\n              if (result.kind === \"terminal_domain\") return {{ kind: \"terminal\", outcome: {{ ok: false, error: decode{operation_name}Error(JSON.stringify(result.value)) }} }};\n              throw dependencyRuntimeError(result.failure);\n            }},\n            async closeSend() {{\n              const result = await stream.closeSend();\n              if (result.kind === \"runtime\") throw dependencyRuntimeError(result.failure);\n            }},\n            cancel() {{ stream.cancel(); }},\n          }} }};\n        }} catch (error) {{\n          return {{ ok: false, error: {{ kind: \"runtime\", error: dependencyFailure(error) }} }};\n        }}\n      }},",
5001                quote_string(&operation.name),
5002            ));
5003        } else if operation.interaction == "event" {
5004            let provider_method = typescript_property_name(&snake_case(&operation.name));
5005            event_dispatch_arms.push(format!(
5006                "      case {}: {{\n        let event: {request_type};\n        try {{\n          event = decode{operation_name}Event(lensoContractRuntime.encodePortableJson(payload, \"event\"));\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"protocol_violation\", detail: providerErrorMessage(error) }} }};\n        }}\n        try {{\n          await provider.{provider_method}(context, event);\n          return {{ kind: \"accepted\" }};\n        }} catch (error) {{\n          return {{ kind: \"runtime\", failure: {{ kind: \"plugin_failure\", detail: providerErrorMessage(error) }} }};\n        }}\n      }}",
5007                quote_string(&operation.name),
5008            ));
5009            dependency_methods.push(format!(
5010                "      async {provider_method}(event, context) {{\n        let payload: unknown;\n        try {{\n          payload = JSON.parse(encode{operation_name}Event(event)) as unknown;\n        }} catch {{\n          return [{{ subscriberInstance: invoke.providerInstance, admission: \"unavailable\" }}];\n        }}\n        const call = context ?? {{ requestId: \"0\" as Uint64, cancelled: false }};\n        try {{\n          const outcome = await invoke.publishEvent({}, call, payload);\n          return [{{ subscriberInstance: invoke.providerInstance, admission: outcome.kind === \"accepted\" ? \"accepted\" : outcome.failure.kind === \"resource_exhausted\" ? \"exhausted\" : \"unavailable\" }}];\n        }} catch {{\n          return [{{ subscriberInstance: invoke.providerInstance, admission: \"unavailable\" }}];\n        }}\n      }},",
5011                quote_string(&operation.name),
5012            ));
5013        }
5014    }
5015    let mut output = String::new();
5016    output.push_str(TYPESCRIPT_HEADER);
5017    output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
5018    writeln!(
5019        output,
5020        "export const CAPABILITY_ID = {};",
5021        quote_string(&contract.capability_id)
5022    )
5023    .expect("writing to a String cannot fail");
5024    writeln!(
5025        output,
5026        "export const DESCRIPTOR_VERSION = {};",
5027        quote_string(&contract.version)
5028    )
5029    .expect("writing to a String cannot fail");
5030    writeln!(
5031        output,
5032        "export const DESCRIPTOR_DIGEST = {};",
5033        quote_string(&contract.descriptor_digest)
5034    )
5035    .expect("writing to a String cannot fail");
5036    writeln!(output, "export const PORTABLE = {};", contract.portable)
5037        .expect("writing to a String cannot fail");
5038    write!(
5039        output,
5040        "export const CROSS_LANE_TRANSFER = {};\n\n",
5041        contract.cross_lane_transfer
5042    )
5043    .expect("writing to a String cannot fail");
5044    output.push_str("export type Int64 = lensoContractRuntime.Int64;\nexport type Uint64 = lensoContractRuntime.Uint64;\nexport type Bytes = lensoContractRuntime.Bytes;\nexport type Timestamp = lensoContractRuntime.Timestamp;\nexport type Duration = lensoContractRuntime.Duration;\nexport type OptionalValue<T> = lensoContractRuntime.OptionalValue<T>;\nexport type InvocationContext = lensoContractRuntime.InvocationContext;\nexport type RuntimeFailure = lensoContractRuntime.RuntimeFailure;\nexport type UnknownDomainError = lensoContractRuntime.UnknownDomainError;\nexport type StreamEvent<Message, DomainError> = lensoContractRuntime.StreamEvent<Message, DomainError>;\nexport type StreamSession<Message, DomainError> = lensoContractRuntime.StreamSession<Message, DomainError>;\n\n");
5045    if has_stream_operations {
5046        output.push_str("export type ProviderStream<Message, DomainError> = StreamSession<Message, DomainError> | AsyncIterable<Message>;\n\nclass ServerOutputStreamInputError extends Error {}\n\nfunction isAsyncIterable<Message>(value: unknown): value is AsyncIterable<Message> {\n  return typeof value === \"object\" && value !== null && Symbol.asyncIterator in value;\n}\n\nfunction lowerProviderStream<Message, DomainError>(stream: ProviderStream<Message, DomainError>): StreamSession<Message, DomainError> {\n  if (!isAsyncIterable<Message>(stream)) return stream;\n  const iterator = stream[Symbol.asyncIterator]();\n  let cancelled = false;\n  return {\n    async send() { throw new ServerOutputStreamInputError(\"server-output stream does not accept inbound messages\"); },\n    async receive() {\n      if (cancelled) return { kind: \"terminal\", outcome: { ok: true } };\n      const next = await iterator.next();\n      return next.done\n        ? { kind: \"terminal\", outcome: { ok: true } }\n        : { kind: \"message\", message: next.value };\n    },\n    async closeSend() {},\n    cancel() {\n      cancelled = true;\n      const closing = iterator.return?.();\n      if (closing !== undefined) void Promise.resolve(closing).catch(() => undefined);\n    },\n  };\n}\n\n");
5047    }
5048    output.push_str("export interface CapabilityContractReference<Client, Provider extends object, Runtime extends DependencyInvoker = DependencyInvoker> extends CapabilityDependencyBinding<Client, Runtime> {\n  readonly kind: \"lenso.capability\";\n  readonly capability_id: string;\n  readonly descriptor_version: string;\n  readonly descriptor_digest: string;\n  readonly generated_client: string;\n  readonly descriptor: CapabilityProviderDescriptor;\n  bindProvider(provider: Provider): CapabilityProviderBinding;\n  required(id?: string): CapabilityDependencyDeclaration<Client, \"one\", Runtime>;\n  optional(id?: string): CapabilityDependencyDeclaration<Client, \"optional\", Runtime>;\n  many(id?: string): CapabilityDependencyDeclaration<Client, \"many\", Runtime>;\n  readonly __client?: Client;\n  readonly __provider?: Provider;\n}\n\n");
5049    if has_event_operations {
5050        output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
5051    }
5052    for declaration in types.declarations {
5053        output.push_str(&declaration);
5054        output.push('\n');
5055    }
5056    for error in errors {
5057        output.push_str(&error);
5058        output.push('\n');
5059    }
5060    for codec in codecs {
5061        output.push_str(&codec);
5062        output.push('\n');
5063    }
5064    write!(
5065        output,
5066        "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
5067        clients.join("\n"),
5068        providers.join("\n")
5069    )
5070    .expect("writing to a String cannot fail");
5071    let dependency_binding = format!("...bind{capability_name}Dependency(), ");
5072    let dependency_declarations = "required(id) { return { kind: \"lenso.dependency\", ...(id === undefined ? {} : { id }), contract: this, cardinality: \"one\" }; }, optional(id) { return { kind: \"lenso.dependency\", ...(id === undefined ? {} : { id }), contract: this, cardinality: \"optional\" }; }, many(id) { return { kind: \"lenso.dependency\", ...(id === undefined ? {} : { id }), contract: this, cardinality: \"many\" }; }, ".to_owned();
5073    writeln!(
5074        output,
5075        "\nexport const {capability_name}: CapabilityContractReference<{capability_name}Client, {capability_name}Provider, {dependency_invoker_type}> = {{ kind: \"lenso.capability\", {dependency_binding}capability_id: CAPABILITY_ID, descriptor_version: DESCRIPTOR_VERSION, descriptor_digest: DESCRIPTOR_DIGEST, generated_client: {}, descriptor: {{ capability_id: CAPABILITY_ID, descriptor_version: DESCRIPTOR_VERSION, operations: [{operation_names}], stream_operations: [{stream_operation_names}], event_operations: [{event_operation_names}] }}, bindProvider: bind{capability_name}Provider, {dependency_declarations}}};\nexport const {capability_const}_CONTRACT = {capability_name};",
5076        quote_string(&format!("{capability_name}Client")),
5077    )
5078    .expect("writing to a String cannot fail");
5079    write!(
5080        output,
5081        "\nexport type ProviderDispatchOutcome =\n  | {{ readonly kind: \"success\"; readonly value: unknown }}\n  | {{ readonly kind: \"domain\"; readonly value: unknown }}\n  | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\nexport type ProviderStreamActionOutcome =\n  | {{ readonly kind: \"accepted\" }}\n  | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\nexport type ProviderStreamReceiveOutcome =\n  | {{ readonly kind: \"message\"; readonly value: unknown }}\n  | {{ readonly kind: \"peer_half_closed\" }}\n  | {{ readonly kind: \"terminal_success\" }}\n  | {{ readonly kind: \"terminal_domain\"; readonly value: unknown }}\n  | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\n/** @internal Runtime lowering seam. */\nexport interface ProviderStreamSessionBinding {{\n  send(message: unknown): Promise<ProviderStreamActionOutcome>;\n  receive(): Promise<ProviderStreamReceiveOutcome>;\n  closeSend(): Promise<ProviderStreamActionOutcome>;\n  cancel(): void;\n}}\nexport type ProviderStreamOpenOutcome =\n  | {{ readonly kind: \"opened\"; readonly stream: ProviderStreamSessionBinding }}\n  | {{ readonly kind: \"domain\"; readonly value: unknown }}\n  | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\nexport type ProviderEventPublishOutcome =\n  | {{ readonly kind: \"accepted\" }}\n  | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\n\nexport interface CapabilityProviderDescriptor {{\n  readonly capability_id: string;\n  readonly descriptor_version: string;\n  readonly operations: ReadonlyArray<string>;\n  readonly stream_operations: ReadonlyArray<string>;\n  readonly event_operations: ReadonlyArray<string>;\n}}\n\n/** @internal Runtime lowering seam. */\nexport interface CapabilityProviderBinding {{\n  readonly descriptor: CapabilityProviderDescriptor;\n  invokeRequest(\n    operation: string,\n    context: InvocationContext,\n    payload: unknown,\n  ): Promise<ProviderDispatchOutcome>;\n  openStream(\n    operation: string,\n    context: InvocationContext,\n    payload: unknown,\n  ): Promise<ProviderStreamOpenOutcome>;\n  publishEvent(\n    operation: string,\n    context: InvocationContext,\n    payload: unknown,\n  ): Promise<ProviderEventPublishOutcome>;\n}}\n\nfunction providerErrorMessage(error: unknown): string {{\n  return error instanceof Error ? error.message : String(error);\n}}\n\nexport function bind{capability_name}Provider(\n  provider: {capability_name}Provider,\n): CapabilityProviderBinding {{\n  return {{\n    descriptor: {{\n      capability_id: CAPABILITY_ID,\n      descriptor_version: DESCRIPTOR_VERSION,\n      operations: [{operation_names}],\n      stream_operations: [{stream_operation_names}],\n      event_operations: [{event_operation_names}],\n    }},\n    async invokeRequest(operation, context, payload) {{\n      switch (operation) {{\n{}\n        default:\n          return {{ kind: \"runtime\", failure: {{ kind: \"unknown_operation\", operation }} }};\n      }}\n    }},\n    async openStream(operation, context, payload) {{\n      switch (operation) {{\n{}\n        default:\n          return {{ kind: \"runtime\", failure: {{ kind: \"unknown_operation\", operation }} }};\n      }}\n    }},\n    async publishEvent(operation, context, payload) {{\n      switch (operation) {{\n{}\n        default:\n          return {{ kind: \"runtime\", failure: {{ kind: \"unknown_operation\", operation }} }};\n      }}\n    }},\n  }};\n}}\n\nexport type Provider = {capability_name}Provider;\nexport const bindProvider = bind{capability_name}Provider;\n",
5082        request_dispatch_arms.join("\n"),
5083        stream_dispatch_arms.join("\n"),
5084        event_dispatch_arms.join("\n"),
5085    )
5086    .expect("writing to a String cannot fail");
5087    write!(
5088            output,
5089            "\nexport type DependencyInvoker = (\n  operation: string,\n  context: InvocationContext,\n  payload: unknown,\n) => Promise<ProviderDispatchOutcome>;\n\nexport type InteractionDependencyInvoker = DependencyInvoker & {{\n  readonly providerInstance: string;\n  openStream(operation: string, context: InvocationContext, payload: unknown): Promise<ProviderStreamOpenOutcome>;\n  publishEvent(operation: string, context: InvocationContext, payload: unknown): Promise<ProviderEventPublishOutcome>;\n}};\n\nexport interface CapabilityDependencyBinding<Client, Runtime extends DependencyInvoker = DependencyInvoker> {{\n  readonly descriptor: CapabilityProviderDescriptor;\n  createClient(invoke: Runtime): Client;\n}}\n\nexport interface CapabilityDependencyDeclaration<Client, Cardinality extends \"one\" | \"optional\" | \"many\", Runtime extends DependencyInvoker = DependencyInvoker> {{\n  readonly kind: \"lenso.dependency\";\n  readonly id?: string;\n  readonly contract: CapabilityDependencyBinding<Client, Runtime>;\n  readonly cardinality: Cardinality;\n}}\n\nfunction dependencyErrorMessage(error: unknown): string {{\n  return error instanceof Error ? error.message : String(error);\n}}\n\nfunction dependencyRuntimeError(failure: RuntimeFailure): Error {{\n  return Object.assign(new Error(`Capability dependency failed: ${{failure.kind}}`), {{ failure }});\n}}\n\nfunction dependencyFailure(error: unknown): RuntimeFailure {{\n  if (typeof error === \"object\" && error !== null && \"failure\" in error) return (error as {{ failure: RuntimeFailure }}).failure;\n  return {{ kind: \"plugin_failure\", detail: dependencyErrorMessage(error) }};\n}}\n\nexport function bind{capability_name}Dependency(): CapabilityDependencyBinding<{capability_name}Client, {dependency_invoker_type}> {{\n  return {{\n    descriptor: {{\n      capability_id: CAPABILITY_ID,\n      descriptor_version: DESCRIPTOR_VERSION,\n      operations: [{operation_names}],\n      stream_operations: [{stream_operation_names}],\n      event_operations: [{event_operation_names}],\n    }},\n    createClient(invoke) {{\n      return {{\n{}\n      }};\n    }},\n  }};\n}}\n\nexport const bindDependency = bind{capability_name}Dependency;\n",
5090            dependency_methods.join("\n"),
5091        )
5092        .expect("writing to a String cannot fail");
5093    output.push_str(
5094        "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
5095    );
5096    output
5097}