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