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