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