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 debug_impl.is_some() {
3465 "Clone, PartialEq, serde::Serialize, serde::Deserialize"
3466 } else {
3467 "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
3468 };
3469 self.declarations[placeholder] = format!(
3470 "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
3471 rendered_fields.join("\n"),
3472 debug_impl = debug_impl.unwrap_or_default(),
3473 );
3474 name.to_owned()
3475 }
3476
3477 fn enum_type(&mut self, name: &str, values: &[String]) -> String {
3478 if !self.declared.insert(name.to_owned()) {
3479 return name.to_owned();
3480 }
3481 let variants = values
3482 .iter()
3483 .map(|value| {
3484 format!(
3485 " #[serde(rename = {})]\n {},",
3486 quote_string(value),
3487 pascal_case(value)
3488 )
3489 })
3490 .collect::<Vec<_>>();
3491 self.declarations.push(format!(
3492 "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
3493 variants.join("\n")
3494 ));
3495 name.to_owned()
3496 }
3497
3498 fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3499 let base = self.type_for_non_null(ty.non_null(), nested_name);
3500 if ty.is_nullable() {
3501 format!("Option<{base}>")
3502 } else {
3503 base
3504 }
3505 }
3506
3507 fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3508 match ty {
3509 TypeIr::Any => "serde_json::Value".to_owned(),
3510 TypeIr::String => "String".to_owned(),
3511 TypeIr::RawJson => "RawJson".to_owned(),
3512 TypeIr::Enum { name, values } => {
3513 self.enum_type(name.as_deref().unwrap_or(nested_name), values)
3514 }
3515 TypeIr::Int64 => "Int64".to_owned(),
3516 TypeIr::Uint64 => "Uint64".to_owned(),
3517 TypeIr::Bytes => "Bytes".to_owned(),
3518 TypeIr::Timestamp => "Timestamp".to_owned(),
3519 TypeIr::Duration => "Duration".to_owned(),
3520 TypeIr::Integer => "i64".to_owned(),
3521 TypeIr::Number => "f64".to_owned(),
3522 TypeIr::Boolean => "bool".to_owned(),
3523 TypeIr::Null => "()".to_owned(),
3524 TypeIr::Array(items) => {
3525 format!(
3526 "Vec<{}>",
3527 self.type_for(items, &format!("{nested_name}Item"))
3528 )
3529 }
3530 TypeIr::Object {
3531 name,
3532 fields,
3533 additional,
3534 } => {
3535 let nested_name = name.as_deref().unwrap_or(nested_name);
3536 if fields.is_empty() {
3537 match additional {
3538 ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3539 ObjectAdditionalIr::Any => {
3540 "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
3541 }
3542 ObjectAdditionalIr::Typed(values) => format!(
3543 "std::collections::BTreeMap<String, {}>",
3544 self.type_for(values, &format!("{nested_name}Value"))
3545 ),
3546 }
3547 } else {
3548 self.object(nested_name, fields)
3549 }
3550 }
3551 TypeIr::Nullable(inner) => {
3552 format!("Option<{}>", self.type_for_non_null(inner, nested_name))
3553 }
3554 }
3555 }
3556}
3557
3558struct TypeScriptTypes {
3559 declarations: Vec<String>,
3560 declared: BTreeSet<String>,
3561}
3562
3563impl TypeScriptTypes {
3564 fn new() -> Self {
3565 Self {
3566 declarations: Vec::new(),
3567 declared: BTreeSet::new(),
3568 }
3569 }
3570
3571 fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3572 if !self.declared.insert(name.to_owned()) {
3573 return name.to_owned();
3574 }
3575 let placeholder = self.declarations.len();
3576 self.declarations.push(String::new());
3577 let mut rendered_fields = Vec::new();
3578 for field in fields {
3579 let type_name = self.type_for_non_null(
3580 field.ty.non_null(),
3581 &format!("{name}{}", pascal_case(&field.name)),
3582 );
3583 let optional = if field.required { "" } else { "?" };
3584 let field_type = if field.ty.is_nullable() {
3585 format!("{type_name} | null")
3586 } else {
3587 type_name
3588 };
3589 rendered_fields.push(format!(
3590 " {}{optional}: {field_type};",
3591 typescript_property_name(&field.name)
3592 ));
3593 }
3594 self.declarations[placeholder] = format!(
3595 "export interface {name} {{\n{}\n}}\n",
3596 rendered_fields.join("\n")
3597 );
3598 name.to_owned()
3599 }
3600
3601 fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3602 let base = self.type_for_non_null(ty.non_null(), nested_name);
3603 if ty.is_nullable() {
3604 format!("{base} | null")
3605 } else {
3606 base
3607 }
3608 }
3609
3610 fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3611 match ty {
3612 TypeIr::Any => "unknown".to_owned(),
3613 TypeIr::String | TypeIr::RawJson => "string".to_owned(),
3614 TypeIr::Int64 => "Int64".to_owned(),
3615 TypeIr::Uint64 => "Uint64".to_owned(),
3616 TypeIr::Bytes => "Bytes".to_owned(),
3617 TypeIr::Timestamp => "Timestamp".to_owned(),
3618 TypeIr::Duration => "Duration".to_owned(),
3619 TypeIr::Integer | TypeIr::Number => "number".to_owned(),
3620 TypeIr::Boolean => "boolean".to_owned(),
3621 TypeIr::Null => "null".to_owned(),
3622 TypeIr::Enum { values, .. } => values
3623 .iter()
3624 .map(|value| quote_string(value))
3625 .collect::<Vec<_>>()
3626 .join(" | "),
3627 TypeIr::Array(items) => {
3628 format!(
3629 "Array<{}>",
3630 self.type_for(items, &format!("{nested_name}Item"))
3631 )
3632 }
3633 TypeIr::Object {
3634 name,
3635 fields,
3636 additional,
3637 } => {
3638 let nested_name = name.as_deref().unwrap_or(nested_name);
3639 if fields.is_empty() {
3640 match additional {
3641 ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3642 ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
3643 ObjectAdditionalIr::Typed(values) => format!(
3644 "Record<string, {}>",
3645 self.type_for(values, &format!("{nested_name}Value"))
3646 ),
3647 }
3648 } else {
3649 self.object(nested_name, fields)
3650 }
3651 }
3652 TypeIr::Nullable(inner) => {
3653 format!("{} | null", self.type_for_non_null(inner, nested_name))
3654 }
3655 }
3656 }
3657}
3658
3659#[allow(clippy::too_many_lines)]
3660fn generate_rust(contract: &ContractIr) -> String {
3661 let capability_name = pascal_case(
3662 contract
3663 .capability_id
3664 .split('@')
3665 .next()
3666 .and_then(|identity| identity.rsplit('.').next())
3667 .unwrap_or("Capability"),
3668 );
3669 let capability_const = screaming_snake_case(&capability_name);
3670 let native_support_name = format!("__LensoNativeSupport{capability_name}");
3671 let mut types = RustTypes::new();
3672 let mut operation_rows = Vec::new();
3673 let mut stream_operation_rows = Vec::new();
3674 let mut event_operation_rows = Vec::new();
3675 let mut operation_markers = Vec::new();
3676 let mut provider_methods = Vec::new();
3677 let mut provider_lowering_methods = Vec::new();
3678 let mut object_provider_lowering_methods = Vec::new();
3679 let mut trait_object_provider_lowering_methods = Vec::new();
3680 let mut provider_result_conversions = Vec::new();
3681 let mut endpoint_arms = Vec::new();
3682 let mut stream_endpoint_arms = Vec::new();
3683 let mut event_endpoint_arms = Vec::new();
3684 let mut client_fields = Vec::new();
3685 let mut client_initializers = Vec::new();
3686 let mut many_client_initializers = Vec::new();
3687 let mut client_methods = Vec::new();
3688 let mut invocation_errors = Vec::new();
3689 let mut error_codecs = Vec::new();
3690 let mut wire_codecs = Vec::new();
3691
3692 for operation in &contract.operations {
3693 let operation_name = pascal_case(&operation.name);
3694 let request_name = format!("{operation_name}Request");
3695 let response_name = format!("{operation_name}Response");
3696 let error_name = format!("{operation_name}Error");
3697 let marker_name = if contract.operations.len() == 1 {
3698 capability_name.clone()
3699 } else {
3700 format!("{capability_name}{operation_name}")
3701 };
3702 let request_type = types.type_for(&operation.request, &request_name);
3703 let response_type = types.type_for(&operation.response, &response_name);
3704 let known_errors = &operation.domain_errors;
3705 let error_definition = if known_errors.is_empty() {
3706 format!(
3707 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n Unknown(UnknownDomainError),\n}}\n"
3708 )
3709 } else {
3710 let variants = known_errors
3711 .iter()
3712 .map(|variant| {
3713 if let Some(payload) = &variant.payload {
3714 let payload_name = format!("{error_name}{}Payload", variant.name);
3715 let payload_type =
3716 types.type_for_non_null(payload.non_null(), &payload_name);
3717 let payload_type = if variant.payload_required {
3718 if payload.is_nullable() {
3719 format!("Option<{payload_type}>")
3720 } else {
3721 payload_type
3722 }
3723 } else if payload.is_nullable() {
3724 format!("OptionalValue<{payload_type}>")
3725 } else {
3726 format!("Option<{payload_type}>")
3727 };
3728 format!(" {} {{ payload: {payload_type} }},", variant.name)
3729 } else {
3730 format!(" {},", variant.name)
3731 }
3732 })
3733 .chain(std::iter::once(
3734 " Unknown(UnknownDomainError),".to_owned(),
3735 ))
3736 .collect::<Vec<_>>();
3737 format!(
3738 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
3739 variants.join("\n")
3740 )
3741 };
3742 types.declarations.push(error_definition);
3743 error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
3744 wire_codecs.push(generate_rust_wire_codecs(
3745 &operation.name,
3746 &request_type,
3747 &response_type,
3748 &error_name,
3749 ));
3750 if operation.interaction == "event" {
3751 wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
3752 }
3753 let operation_const = screaming_snake_case(&operation.name);
3754 operation_markers.push(match operation.interaction.as_str() {
3755 "request" => {
3756 let provider_method = rust_field_name(&operation.name);
3757 format!(
3758 "#[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"
3759 )
3760 }
3761 "stream" => format!(
3762 "#[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"
3763 ),
3764 "event" => format!(
3765 "#[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"
3766 ),
3767 _ => unreachable!("Descriptor validation restricts interactions"),
3768 });
3769 if operation.interaction == "request" {
3770 let invocation_error_name = if contract.operations.len() == 1 {
3771 format!("{capability_name}InvocationError")
3772 } else {
3773 format!("{capability_name}{operation_name}InvocationError")
3774 };
3775 operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
3776 provider_methods.push(format!(
3777 " fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
3778 rust_field_name(&operation.name),
3779 ));
3780 let field = rust_field_name(&operation.name);
3781 let conversion = format!("__LensoInto{capability_name}{operation_name}Result");
3782 provider_result_conversions.push(format!(
3783 "#[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"
3784 ));
3785 provider_lowering_methods.push(format!(
3786 " fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n let 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 }}"
3787 ));
3788 object_provider_lowering_methods.push(format!(
3789 " fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n let object = self.clone();\n ::std::boxed::Box::pin(async move {{\n let plugin = object.get()?;\n let result = <$plugin>::{field}(plugin.as_ref(), context, request).await;\n $crate::{conversion}::__lenso_into_result(result)\n }})\n }}"
3790 ));
3791 trait_object_provider_lowering_methods.push(format!(
3792 " 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 }}"
3793 ));
3794 endpoint_arms.push(format!(
3795 " {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 }}",
3796 rust_field_name(&operation.name),
3797 ));
3798 let field = rust_field_name(&operation.name);
3799 client_fields.push(format!(" {field}: NativeRequestHandle<{marker_name}>,"));
3800 client_initializers.push(format!(
3801 " {field}: dependencies.one::<{marker_name}>()?,"
3802 ));
3803 many_client_initializers.push(format!(
3804 " {field}: binding.handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3805 ));
3806 client_methods.push(format!(
3807 " 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 }}",
3808 screaming_snake_case(&operation.name),
3809 screaming_snake_case(&operation.name),
3810 ));
3811 invocation_errors.push(format!(
3812 "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n Domain({error_name}),\n Runtime(RuntimeFailure),\n}}\n"
3813 ));
3814 } else if operation.interaction == "stream" {
3815 let invocation_error_name = if contract.operations.len() == 1 {
3816 format!("{capability_name}InvocationError")
3817 } else {
3818 format!("{capability_name}{operation_name}InvocationError")
3819 };
3820 stream_operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
3821 provider_methods.push(format!(
3822 " fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
3823 rust_field_name(&operation.name)
3824 ));
3825 let field = rust_field_name(&operation.name);
3826 let conversion = format!("__LensoInto{capability_name}{operation_name}StreamResult");
3827 provider_result_conversions.push(format!(
3828 "#[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"
3829 ));
3830 provider_lowering_methods.push(format!(
3831 " fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n let 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 }}"
3832 ));
3833 object_provider_lowering_methods.push(format!(
3834 " fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<Box<dyn {native_support_name}::NativeStreamSession>, $crate::{invocation_error_name}>> {{\n let object = self.clone();\n ::std::boxed::Box::pin(async move {{\n let plugin = object.get().map_err($crate::{invocation_error_name}::Runtime)?;\n let result = <$plugin>::{field}(plugin.as_ref(), context, request).await;\n $crate::{conversion}::__lenso_into_result(result)\n }})\n }}"
3835 ));
3836 trait_object_provider_lowering_methods.push(format!(
3837 " 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 }}"
3838 ));
3839 stream_endpoint_arms.push(format!(
3840 " {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 }}",
3841 rust_field_name(&operation.name),
3842 ));
3843 let field = rust_field_name(&operation.name);
3844 client_fields.push(format!(" {field}: NativeStreamHandle<{marker_name}>,"));
3845 client_initializers.push(format!(
3846 " {field}: dependencies.one_stream::<{marker_name}>()?,"
3847 ));
3848 many_client_initializers.push(format!(
3849 " {field}: binding.stream_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3850 ));
3851 client_methods.push(format!(
3852 " 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 }}"
3853 ));
3854 invocation_errors.push(format!(
3855 "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n Domain({error_name}),\n Runtime(RuntimeFailure),\n}}\n"
3856 ));
3857 } else {
3858 event_operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
3859 provider_methods.push(format!(
3860 " fn {}(&self, context: InvocationContext, event: {request_type}) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;",
3861 rust_field_name(&operation.name)
3862 ));
3863 let field = rust_field_name(&operation.name);
3864 let conversion = format!("__LensoInto{capability_name}{operation_name}EventResult");
3865 provider_result_conversions.push(format!(
3866 "#[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"
3867 ));
3868 provider_lowering_methods.push(format!(
3869 " fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n let 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 }}"
3870 ));
3871 object_provider_lowering_methods.push(format!(
3872 " fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n let object = self.clone();\n ::std::boxed::Box::pin(async move {{\n let plugin = object.get()?;\n let result = <$plugin>::{field}(plugin.as_ref(), context, event).await;\n $crate::{conversion}::__lenso_into_result(result)\n }})\n }}"
3873 ));
3874 trait_object_provider_lowering_methods.push(format!(
3875 " 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 }}"
3876 ));
3877 event_endpoint_arms.push(format!(
3878 " {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 }}",
3879 rust_field_name(&operation.name),
3880 ));
3881 let field = rust_field_name(&operation.name);
3882 client_fields.push(format!(" {field}: NativeEventHandle<{marker_name}>,"));
3883 client_initializers.push(format!(
3884 " {field}: dependencies.many_event::<{marker_name}>()?,"
3885 ));
3886 many_client_initializers.push(format!(
3887 " {field}: binding.event_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3888 ));
3889 client_methods.push(format!(
3890 " 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 }}"
3891 ));
3892 }
3893 }
3894
3895 let request_endpoint_impl = if operation_rows.is_empty() {
3896 String::new()
3897 } else {
3898 format!(
3899 "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",
3900 operation_rows.concat(),
3901 endpoint_arms.join(",\n")
3902 )
3903 };
3904 let stream_endpoint_impl = if stream_operation_rows.is_empty() {
3905 String::new()
3906 } else {
3907 format!(
3908 "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",
3909 stream_operation_rows.concat(),
3910 stream_endpoint_arms.join(",\n")
3911 )
3912 };
3913 let event_endpoint_impl = if event_operation_rows.is_empty() {
3914 String::new()
3915 } else {
3916 format!(
3917 "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",
3918 event_operation_rows.concat(),
3919 event_endpoint_arms.join(",\n")
3920 )
3921 };
3922 let mut output = String::new();
3923 output.push_str(GENERATED_HEADER);
3924 let has_request_operations = !operation_rows.is_empty();
3925 let has_stream_operations = !stream_operation_rows.is_empty();
3926 let has_event_operations = !event_operation_rows.is_empty();
3927 let mut kernel_imports = vec!["InvocationContext", "PluginDependencies", "RuntimeFailure"];
3928 if has_request_operations {
3929 kernel_imports.extend([
3930 "NativeRequestFuture",
3931 "NativeRequestEndpoint",
3932 "NativeRequestHandle",
3933 "RequestCapability",
3934 ]);
3935 }
3936 if has_stream_operations {
3937 kernel_imports.extend([
3938 "NativeStream",
3939 "NativeStreamEndpoint",
3940 "NativeStreamHandle",
3941 "NativeStreamSession",
3942 "StreamCapability",
3943 "StreamEvent",
3944 ]);
3945 }
3946 if has_event_operations {
3947 kernel_imports.extend([
3948 "EventCapability",
3949 "EventPublishResult",
3950 "NativeEventEndpoint",
3951 "NativeEventHandle",
3952 ]);
3953 }
3954 kernel_imports.sort_unstable();
3955 writeln!(
3956 output,
3957 "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
3958 kernel_imports.join(", ")
3959 )
3960 .expect("writing to a String cannot fail");
3961 output.push_str(
3962 "use lenso_plugin_authoring::{BoundCapabilityClient, CapabilityClient, CapabilityClientMany, CapabilityReference};\n",
3963 );
3964 writeln!(
3965 output,
3966 "pub const CAPABILITY_ID: &str = {};",
3967 quote_string(&contract.capability_id)
3968 )
3969 .expect("writing to a String cannot fail");
3970 writeln!(
3971 output,
3972 "pub const DESCRIPTOR_VERSION: &str = {};",
3973 quote_string(&contract.version)
3974 )
3975 .expect("writing to a String cannot fail");
3976 writeln!(
3977 output,
3978 "pub const DESCRIPTOR_DIGEST: &str = {};",
3979 quote_string(&contract.descriptor_digest)
3980 )
3981 .expect("writing to a String cannot fail");
3982 writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
3983 .expect("writing to a String cannot fail");
3984 writeln!(
3985 output,
3986 "pub const CROSS_LANE_TRANSFER: bool = {};",
3987 contract.cross_lane_transfer
3988 )
3989 .expect("writing to a String cannot fail");
3990 writeln!(
3991 output,
3992 "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
3993 )
3994 .expect("writing to a String cannot fail");
3995 write!(
3996 output,
3997 "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"
3998 )
3999 .expect("writing to a String cannot fail");
4000
4001 let capability_macro_name = snake_case(&capability_name);
4002 let client_macro_name = snake_case(&format!("{capability_name}Client"));
4003 let operations = contract
4004 .operations
4005 .iter()
4006 .map(|operation| Value::String(operation.name.clone()))
4007 .collect::<Vec<_>>();
4008 let operation_kinds = contract
4009 .operations
4010 .iter()
4011 .filter(|operation| operation.interaction != "request")
4012 .map(|operation| {
4013 (
4014 operation.name.clone(),
4015 Value::String(operation.interaction.clone()),
4016 )
4017 })
4018 .collect::<Map<_, _>>();
4019 let provided_fragment = canonical_json(&serde_json::json!({
4020 "capability_id": contract.capability_id,
4021 "descriptor_version": contract.version,
4022 "operations": operations,
4023 "operation_kinds": operation_kinds,
4024 "default_admission": {
4025 "queue_capacity": 0,
4026 "max_concurrency": 1
4027 },
4028 "operation_admissions": {},
4029 "event_admission": null,
4030 "cross_lane_transfer": contract.cross_lane_transfer
4031 }));
4032 let required_fragment = canonical_json(&serde_json::json!({
4033 "capability_id": contract.capability_id,
4034 "descriptor_version": contract.version,
4035 "cardinality": "one"
4036 }));
4037 let required_many_fragment = canonical_json(&serde_json::json!({
4038 "capability_id": contract.capability_id,
4039 "descriptor_version": contract.version,
4040 "cardinality": "many"
4041 }));
4042 let named_requirement_suffix = |cardinality: &str| {
4043 format!(
4044 ",\"capability_id\":{},\"descriptor_version\":{},\"cardinality\":{}}}",
4045 quote_string(&contract.capability_id),
4046 quote_string(&contract.version),
4047 quote_string(cardinality),
4048 )
4049 };
4050 let required_named_suffix = named_requirement_suffix("one");
4051 let required_optional_named_suffix = named_requirement_suffix("optional");
4052 let required_many_named_suffix = named_requirement_suffix("many");
4053 writeln!(
4054 output,
4055 "#[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",
4056 quote_string(&provided_fragment),
4057 quote_string(&required_fragment),
4058 quote_string(&required_named_suffix),
4059 quote_string(&required_optional_named_suffix),
4060 quote_string(&required_many_fragment),
4061 quote_string(&required_many_named_suffix),
4062 )
4063 .expect("writing to a String cannot fail");
4064 for operation in &contract.operations {
4065 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4066 writeln!(
4067 output,
4068 "pub const {operation_const}: &str = {};",
4069 quote_string(&operation.name)
4070 )
4071 .expect("writing to a String cannot fail");
4072 }
4073 writeln!(
4074 output,
4075 "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
4076 rust_runtime_types(contract).join(", ")
4077 )
4078 .expect("writing to a String cannot fail");
4079 for declaration in types.declarations {
4080 output.push_str(&declaration);
4081 output.push('\n');
4082 }
4083 for marker in operation_markers {
4084 output.push_str(&marker);
4085 output.push('\n');
4086 }
4087 for codec in error_codecs {
4088 output.push_str(&codec);
4089 output.push('\n');
4090 }
4091 for codec in wire_codecs {
4092 output.push_str(&codec);
4093 output.push('\n');
4094 }
4095 for conversion in provider_result_conversions {
4096 output.push_str(&conversion);
4097 output.push('\n');
4098 }
4099 write!(
4100 output,
4101 "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
4102 provider_methods.join("\n")
4103 )
4104 .expect("writing to a String cannot fail");
4105 writeln!(
4106 output,
4107 "#[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",
4108 provider_lowering_methods.join("\n"),
4109 object_provider_lowering_methods.join("\n"),
4110 trait_object_provider_lowering_methods.join("\n")
4111 )
4112 .expect("writing to a String cannot fail");
4113 if has_request_operations {
4114 write!(
4115 output,
4116 "#[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"
4117 )
4118 .expect("writing to a String cannot fail");
4119 } else {
4120 write!(
4121 output,
4122 "#[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"
4123 )
4124 .expect("writing to a String cannot fail");
4125 }
4126 output.push_str(&request_endpoint_impl);
4127 output.push_str(&stream_endpoint_impl);
4128 output.push_str(&event_endpoint_impl);
4129 let request_endpoint_value = has_request_operations.then_some(
4130 "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeRequestEndpoint>",
4131 );
4132 let stream_endpoint_value = has_stream_operations.then_some(
4133 "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeStreamEndpoint>",
4134 );
4135 let event_endpoint_value = has_event_operations
4136 .then_some("endpoint as ::std::rc::Rc<dyn __LensoNativeSupport::NativeEventEndpoint>");
4137 writeln!(
4138 output,
4139 "#[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",
4140 request_endpoint_value.unwrap_or_default(),
4141 stream_endpoint_value.unwrap_or_default(),
4142 event_endpoint_value.unwrap_or_default(),
4143 )
4144 .expect("writing to a String cannot fail");
4145 let new_method = if contract.operations.len() == 1 {
4146 let field = rust_field_name(&contract.operations[0].name);
4147 let marker = &capability_name;
4148 match contract.operations[0].interaction.as_str() {
4149 "request" => format!(
4150 " pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
4151 ),
4152 "stream" => format!(
4153 " pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
4154 ),
4155 "event" => format!(
4156 " pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
4157 ),
4158 _ => unreachable!("Descriptor validation restricts interactions"),
4159 }
4160 } else {
4161 String::new()
4162 };
4163 write!(
4164 output,
4165 "#[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",
4166 client_fields.join("\n"),
4167 new_method,
4168 client_methods.join("\n\n"),
4169 client_initializers.join("\n"),
4170 many_client_initializers.join("\n")
4171 )
4172 .expect("writing to a String cannot fail");
4173 for error in invocation_errors {
4174 output.push_str(&error);
4175 }
4176 format!("{}\n", output.trim_end())
4177}
4178
4179#[allow(clippy::too_many_lines)]
4180fn generate_rust_plugin(contract: &ContractIr) -> Result<String, CodegenError> {
4181 let capability_name = pascal_case(
4182 contract
4183 .capability_id
4184 .split('@')
4185 .next()
4186 .and_then(|identity| identity.rsplit('.').next())
4187 .unwrap_or("Capability"),
4188 );
4189 let capability_const = screaming_snake_case(&capability_name);
4190 let mut types = RustTypes::new();
4191 let mut methods = Vec::new();
4192 let mut provider_methods = Vec::new();
4193 let mut dispatch_arms = Vec::new();
4194 let mut error_codecs = Vec::new();
4195 let mut wire_codecs = Vec::new();
4196
4197 for operation in &contract.operations {
4198 if operation.interaction != "request" {
4199 return Err(CodegenError::UnsupportedInteraction {
4200 operation: operation.name.clone(),
4201 interaction: operation.interaction.clone(),
4202 });
4203 }
4204 let operation_name = pascal_case(&operation.name);
4205 let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
4206 let response_type =
4207 types.type_for(&operation.response, &format!("{operation_name}Response"));
4208 let error_name = format!("{operation_name}Error");
4209 let known_errors = &operation.domain_errors;
4210 let error_definition = if known_errors.is_empty() {
4211 format!(
4212 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n Unknown(UnknownDomainError),\n}}\n"
4213 )
4214 } else {
4215 let variants = known_errors
4216 .iter()
4217 .map(|variant| {
4218 if let Some(payload) = &variant.payload {
4219 let payload_name = format!("{error_name}{}Payload", variant.name);
4220 let payload_type =
4221 types.type_for_non_null(payload.non_null(), &payload_name);
4222 let payload_type = if variant.payload_required {
4223 if payload.is_nullable() {
4224 format!("Option<{payload_type}>")
4225 } else {
4226 payload_type
4227 }
4228 } else if payload.is_nullable() {
4229 format!("OptionalValue<{payload_type}>")
4230 } else {
4231 format!("Option<{payload_type}>")
4232 };
4233 format!(" {} {{ payload: {payload_type} }},", variant.name)
4234 } else {
4235 format!(" {},", variant.name)
4236 }
4237 })
4238 .chain(std::iter::once(
4239 " Unknown(UnknownDomainError),".to_owned(),
4240 ))
4241 .collect::<Vec<_>>();
4242 format!(
4243 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
4244 variants.join("\n")
4245 )
4246 };
4247 types.declarations.push(error_definition);
4248 error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
4249 wire_codecs.push(generate_rust_wire_codecs(
4250 &operation.name,
4251 &request_type,
4252 &response_type,
4253 &error_name,
4254 ));
4255 let method = rust_field_name(&operation.name);
4256 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4257 methods.push(format!(
4258 " 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 }}"
4259 ));
4260 provider_methods.push(format!(
4261 " fn {method}(\n &self,\n context: lenso_plugin_sdk::Ctx,\n request: {request_type},\n ) -> Result<{response_type}, {error_name}>;"
4262 ));
4263 dispatch_arms.push(format!(
4264 " $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 }}"
4265 ));
4266 }
4267
4268 let mut output = String::new();
4269 output.push_str(GENERATED_HEADER);
4270 writeln!(
4271 output,
4272 "pub const CAPABILITY_ID: &str = {};",
4273 quote_string(&contract.capability_id)
4274 )
4275 .expect("writing to a String cannot fail");
4276 writeln!(
4277 output,
4278 "pub const DESCRIPTOR_VERSION: &str = {};",
4279 quote_string(&contract.version)
4280 )
4281 .expect("writing to a String cannot fail");
4282 writeln!(
4283 output,
4284 "pub const DESCRIPTOR_DIGEST: &str = {};",
4285 quote_string(&contract.descriptor_digest)
4286 )
4287 .expect("writing to a String cannot fail");
4288 writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
4289 .expect("writing to a String cannot fail");
4290 writeln!(
4291 output,
4292 "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"
4293 )
4294 .expect("writing to a String cannot fail");
4295 for operation in &contract.operations {
4296 writeln!(
4297 output,
4298 "pub const {}_OPERATION: &str = {};",
4299 screaming_snake_case(&operation.name),
4300 quote_string(&operation.name)
4301 )
4302 .expect("writing to a String cannot fail");
4303 }
4304 writeln!(
4305 output,
4306 "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
4307 rust_runtime_types(contract).join(", ")
4308 )
4309 .expect("writing to a String cannot fail");
4310 for declaration in types.declarations {
4311 output.push_str(&declaration);
4312 output.push('\n');
4313 }
4314 for codec in error_codecs {
4315 output.push_str(&codec);
4316 output.push('\n');
4317 }
4318 for codec in wire_codecs {
4319 output.push_str(&codec);
4320 output.push('\n');
4321 }
4322 write!(
4323 output,
4324 "#[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",
4325 methods.join("\n\n")
4326 )
4327 .expect("writing to a String cannot fail");
4328 let export_macro = format!("export_{}_plugin", snake_case(&capability_name));
4329 let request_literals = contract
4330 .operations
4331 .iter()
4332 .map(|operation| quote_string(&operation.name))
4333 .collect::<Vec<_>>()
4334 .join(", ");
4335 write!(
4336 output,
4337 "\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",
4338 provider_methods.join("\n\n"),
4339 dispatch_arms.join(",\n"),
4340 quote_string(&contract.capability_id),
4341 quote_string(&contract.version),
4342 quote_string(&contract.descriptor_digest),
4343 )
4344 .expect("writing to a String cannot fail");
4345 Ok(format!("{}\n", output.trim_end()))
4346}
4347
4348fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
4349 let mut types = BTreeSet::from(["UnknownDomainError"]);
4350 for operation in &contract.operations {
4351 collect_rust_runtime_types(&operation.request, &mut types);
4352 collect_rust_runtime_types(&operation.response, &mut types);
4353 for error in &operation.domain_errors {
4354 if let Some(payload) = &error.payload {
4355 if !error.payload_required && payload.is_nullable() {
4356 types.insert("OptionalValue");
4357 }
4358 collect_rust_runtime_types(payload, &mut types);
4359 }
4360 }
4361 }
4362 types.into_iter().collect()
4363}
4364
4365fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
4366 match ty {
4367 TypeIr::Int64 => {
4368 types.insert("Int64");
4369 }
4370 TypeIr::Uint64 => {
4371 types.insert("Uint64");
4372 }
4373 TypeIr::Bytes => {
4374 types.insert("Bytes");
4375 }
4376 TypeIr::Timestamp => {
4377 types.insert("Timestamp");
4378 }
4379 TypeIr::Duration => {
4380 types.insert("Duration");
4381 }
4382 TypeIr::RawJson => {
4383 types.insert("RawJson");
4384 }
4385 TypeIr::Array(item) | TypeIr::Nullable(item) => {
4386 collect_rust_runtime_types(item, types);
4387 }
4388 TypeIr::Object {
4389 fields, additional, ..
4390 } => {
4391 for field in fields {
4392 if !field.required && field.ty.is_nullable() {
4393 types.insert("OptionalValue");
4394 }
4395 collect_rust_runtime_types(&field.ty, types);
4396 }
4397 if let ObjectAdditionalIr::Typed(value) = additional {
4398 collect_rust_runtime_types(value, types);
4399 }
4400 }
4401 TypeIr::Any
4402 | TypeIr::String
4403 | TypeIr::Integer
4404 | TypeIr::Number
4405 | TypeIr::Boolean
4406 | TypeIr::Null
4407 | TypeIr::Enum { .. } => {}
4408 }
4409}
4410
4411#[allow(clippy::too_many_lines)]
4412fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
4413 let mut output = String::new();
4414 writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
4415 output.push_str(
4416 " fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n where\n S: serde::Serializer,\n {\n",
4417 );
4418 output.push_str(" use serde::ser::SerializeMap;\n");
4419 output.push_str(" match self {\n");
4420 for variant in variants {
4421 if variant.structured {
4422 if variant.payload.is_some() {
4423 if variant.payload_required {
4424 writeln!(
4425 output,
4426 " 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 }},",
4427 variant.name,
4428 quote_string(&variant.code)
4429 )
4430 .expect("String cannot fail");
4431 } else {
4432 writeln!(
4433 output,
4434 " 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 }},",
4435 variant.name,
4436 quote_string(&variant.code)
4437 )
4438 .expect("String cannot fail");
4439 }
4440 } else {
4441 writeln!(
4442 output,
4443 " Self::{} => {{\n let mut map = serializer.serialize_map(Some(1))?;\n map.serialize_entry(\"code\", {})?;\n map.end()\n }},",
4444 variant.name,
4445 quote_string(&variant.code)
4446 )
4447 .expect("String cannot fail");
4448 }
4449 } else {
4450 writeln!(
4451 output,
4452 " Self::{} => serializer.serialize_str({}),",
4453 variant.name,
4454 quote_string(&variant.code)
4455 )
4456 .expect("String cannot fail");
4457 }
4458 }
4459 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");
4460 writeln!(
4461 output,
4462 "impl<'de> serde::Deserialize<'de> for {error_name} {{"
4463 )
4464 .expect("String cannot fail");
4465 output.push_str(
4466 " 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",
4467 );
4468 for variant in variants.iter().filter(|variant| !variant.structured) {
4469 writeln!(
4470 output,
4471 " {} => Ok(Self::{}),",
4472 quote_string(&variant.code),
4473 variant.name
4474 )
4475 .expect("String cannot fail");
4476 }
4477 output.push_str(
4478 " _ => 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",
4479 );
4480 if variants.iter().any(|variant| variant.structured) {
4481 output.push_str(" match code.as_str() {\n");
4482 for variant in variants.iter().filter(|variant| variant.structured) {
4483 if variant.payload.is_some() {
4484 if variant.payload_required {
4485 writeln!(
4486 output,
4487 " {} => {{\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 }},",
4488 quote_string(&variant.code),
4489 variant.name
4490 )
4491 .expect("String cannot fail");
4492 } else {
4493 writeln!(
4494 output,
4495 " {} => {{\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 }},",
4496 quote_string(&variant.code),
4497 variant.name
4498 )
4499 .expect("String cannot fail");
4500 }
4501 } else {
4502 writeln!(
4503 output,
4504 " {} => Ok(Self::{}),",
4505 quote_string(&variant.code),
4506 variant.name
4507 )
4508 .expect("String cannot fail");
4509 }
4510 }
4511 output.push_str(
4512 " _ => {\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",
4513 );
4514 } else {
4515 output.push_str(
4516 " 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",
4517 );
4518 }
4519 output.push_str(
4520 " other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n }\n }\n}\n",
4521 );
4522 output
4523}
4524
4525#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
4526fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
4527 let capability_name = pascal_case(
4528 contract
4529 .capability_id
4530 .split('@')
4531 .next()
4532 .and_then(|identity| identity.rsplit('.').next())
4533 .unwrap_or("Capability"),
4534 );
4535 let codec_name = format!("{capability_name}JsonCodec");
4536 let guest_client_name = format!("{capability_name}GuestClient");
4537 let mut types = RustTypes::new();
4538 let mut request_operations = Vec::new();
4539 let mut stream_operations = Vec::new();
4540 let mut event_operations = Vec::new();
4541 let mut encode_arms = Vec::new();
4542 let mut response_arms = Vec::new();
4543 let mut error_arms = Vec::new();
4544 let mut host_request_arms = Vec::new();
4545 let mut stream_open_arms = Vec::new();
4546 let mut stream_encode_arms = Vec::new();
4547 let mut stream_decode_arms = Vec::new();
4548 let mut stream_error_arms = Vec::new();
4549 let mut host_stream_arms = Vec::new();
4550 let mut event_encode_arms = Vec::new();
4551 let mut host_event_arms = Vec::new();
4552 let mut guest_methods = Vec::new();
4553
4554 for operation in &contract.operations {
4555 let operation_name = pascal_case(&operation.name);
4556 let marker_name = if contract.operations.len() == 1 {
4557 capability_name.clone()
4558 } else {
4559 format!("{capability_name}{operation_name}")
4560 };
4561 let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
4562 let response_type =
4563 types.type_for(&operation.response, &format!("{operation_name}Response"));
4564 let error_type = format!("{operation_name}Error");
4565 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4566 let method_name = rust_field_name(&operation.name);
4567 match operation.interaction.as_str() {
4568 "request" => {
4569 request_operations.push(operation_const.clone());
4570 encode_arms.push(format!(
4571 " {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 }}"
4572 ));
4573 response_arms.push(format!(
4574 " {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()),"
4575 ));
4576 error_arms.push(format!(
4577 " {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()),"
4578 ));
4579 host_request_arms.push(format!(
4580 " {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 }}"
4581 ));
4582 guest_methods.push(format!(
4583 " 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 }}"
4584 ));
4585 }
4586 "stream" => {
4587 stream_operations.push(operation_const.clone());
4588 stream_open_arms.push(format!(
4589 " {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 }}"
4590 ));
4591 stream_encode_arms.push(format!(
4592 " {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 }}"
4593 ));
4594 stream_decode_arms.push(format!(
4595 " {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()),"
4596 ));
4597 stream_error_arms.push(format!(
4598 " {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()),"
4599 ));
4600 host_stream_arms.push(format!(
4601 " {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 }}"
4602 ));
4603 guest_methods.push(format!(
4604 " 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 }}"
4605 ));
4606 }
4607 "event" => {
4608 event_operations.push(operation_const.clone());
4609 event_encode_arms.push(format!(
4610 " {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 }}"
4611 ));
4612 host_event_arms.push(format!(
4613 " {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 }}"
4614 ));
4615 guest_methods.push(format!(
4616 " 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 }}"
4617 ));
4618 }
4619 _ => unreachable!("Descriptor validation restricts interactions"),
4620 }
4621 }
4622
4623 let encode_dispatch = runtime_codec_dispatch(&encode_arms);
4624 let response_dispatch = runtime_codec_dispatch(&response_arms);
4625 let error_dispatch = runtime_codec_dispatch(&error_arms);
4626 let host_request_dispatch = runtime_codec_host_dispatch(&host_request_arms);
4627 let stream_open_dispatch = runtime_codec_dispatch(&stream_open_arms);
4628 let stream_encode_dispatch = runtime_codec_dispatch(&stream_encode_arms);
4629 let stream_decode_dispatch = runtime_codec_dispatch(&stream_decode_arms);
4630 let stream_error_dispatch = runtime_codec_dispatch(&stream_error_arms);
4631 let host_stream_dispatch = runtime_codec_host_dispatch(&host_stream_arms);
4632 let event_encode_dispatch = runtime_codec_dispatch(&event_encode_arms);
4633 let host_event_dispatch = runtime_codec_host_dispatch(&host_event_arms);
4634 let request_parameter = if request_operations.is_empty() {
4635 "_request"
4636 } else {
4637 "request"
4638 };
4639 let request_value_parameter = if request_operations.is_empty() {
4640 "_value"
4641 } else {
4642 "value"
4643 };
4644 let stream_request_parameter = if stream_operations.is_empty() {
4645 "_request"
4646 } else {
4647 "request"
4648 };
4649 let stream_message_parameter = if stream_operations.is_empty() {
4650 "_message"
4651 } else {
4652 "message"
4653 };
4654 let stream_value_parameter = if stream_operations.is_empty() {
4655 "_value"
4656 } else {
4657 "value"
4658 };
4659 let event_parameter = if event_operations.is_empty() {
4660 "_event"
4661 } else {
4662 "event"
4663 };
4664 let (host_request_dependency_parameter, host_request_parameter, host_request_context_parameter) =
4665 if request_operations.is_empty() {
4666 ("_dependency", "_request", "_context")
4667 } else {
4668 ("dependency", "request", "context")
4669 };
4670 let (host_stream_dependency_parameter, host_stream_parameter, host_stream_context_parameter) =
4671 if stream_operations.is_empty() {
4672 ("_dependency", "_request", "_context")
4673 } else {
4674 ("dependency", "request", "context")
4675 };
4676 let (host_event_dependency_parameter, host_event_parameter, host_event_context_parameter) =
4677 if event_operations.is_empty() {
4678 ("_dependency", "_event", "_context")
4679 } else {
4680 ("dependency", "event", "context")
4681 };
4682
4683 let mut output = generate_rust(contract);
4684 write!(
4685 output,
4686 "\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",
4687 request_operations.join(", "),
4688 stream_operations.join(", "),
4689 guest_methods.join("\n\n"),
4690 )
4691 .expect("writing generated Rust to a String cannot fail");
4692 write!(
4693 output,
4694 "\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",
4695 request_operations.join(", "),
4696 stream_operations.join(", "),
4697 event_operations.join(", "),
4698 )
4699 .expect("writing generated Rust to a String cannot fail");
4700 Ok(output)
4701}
4702
4703fn runtime_codec_host_dispatch(arms: &[String]) -> String {
4704 if arms.is_empty() {
4705 " Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation))))"
4706 .to_owned()
4707 } else {
4708 let arms = arms
4709 .iter()
4710 .map(|arm| arm.trim_end_matches(','))
4711 .collect::<Vec<_>>()
4712 .join(",\n");
4713 format!(
4714 " match operation.as_str() {{\n{arms},\n _ => Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation)))),\n }}"
4715 )
4716 }
4717}
4718
4719fn runtime_codec_dispatch(arms: &[String]) -> String {
4720 if arms.is_empty() {
4721 " Err(runtime_codec_unknown_operation(operation))".to_owned()
4722 } else {
4723 let arms = arms
4724 .iter()
4725 .map(|arm| arm.trim_end_matches(','))
4726 .collect::<Vec<_>>()
4727 .join(",\n");
4728 format!(
4729 " match operation {{\n{arms},\n _ => Err(runtime_codec_unknown_operation(operation)),\n }}"
4730 )
4731 }
4732}
4733
4734fn generate_rust_wire_codecs(
4735 operation: &str,
4736 request_type: &str,
4737 response_type: &str,
4738 error_type: &str,
4739) -> String {
4740 let stem = snake_case(operation);
4741 format!(
4742 "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"
4743 )
4744}
4745
4746fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
4747 let stem = snake_case(operation);
4748 format!(
4749 "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"
4750 )
4751}
4752
4753fn generate_typescript_codecs(
4754 operation: &str,
4755 request_type: &str,
4756 response_type: &str,
4757 error_type: &str,
4758 variants: &[ErrorVariantIr],
4759) -> String {
4760 let stem = pascal_case(operation);
4761 let known_strings = variants
4762 .iter()
4763 .filter(|variant| !variant.structured)
4764 .map(|variant| quote_string(&variant.code))
4765 .collect::<Vec<_>>()
4766 .join(", ");
4767 format!(
4768 "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"
4769 )
4770}
4771
4772fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
4773 let stem = pascal_case(operation);
4774 format!(
4775 "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"
4776 )
4777}
4778
4779#[allow(clippy::too_many_lines)]
4780fn generate_typescript(contract: &ContractIr) -> String {
4781 let capability_name = pascal_case(
4782 contract
4783 .capability_id
4784 .split('@')
4785 .next()
4786 .and_then(|identity| identity.rsplit('.').next())
4787 .unwrap_or("Capability"),
4788 );
4789 let capability_const = screaming_snake_case(&capability_name);
4790 let mut types = TypeScriptTypes::new();
4791 let mut clients = Vec::new();
4792 let mut providers = Vec::new();
4793 let mut errors = Vec::new();
4794 let mut codecs = Vec::new();
4795 let mut request_dispatch_arms = Vec::new();
4796 let mut stream_dispatch_arms = Vec::new();
4797 let mut event_dispatch_arms = Vec::new();
4798 let mut dependency_methods = Vec::new();
4799 let operation_names = contract
4800 .operations
4801 .iter()
4802 .map(|operation| quote_string(&operation.name))
4803 .collect::<Vec<_>>()
4804 .join(", ");
4805 let stream_operation_names = contract
4806 .operations
4807 .iter()
4808 .filter(|operation| operation.interaction == "stream")
4809 .map(|operation| quote_string(&operation.name))
4810 .collect::<Vec<_>>()
4811 .join(", ");
4812 let event_operation_names = contract
4813 .operations
4814 .iter()
4815 .filter(|operation| operation.interaction == "event")
4816 .map(|operation| quote_string(&operation.name))
4817 .collect::<Vec<_>>()
4818 .join(", ");
4819 let has_stream_operations = contract
4820 .operations
4821 .iter()
4822 .any(|operation| operation.interaction == "stream");
4823 let has_event_operations = contract
4824 .operations
4825 .iter()
4826 .any(|operation| operation.interaction == "event");
4827 let dependency_invoker_type = if has_stream_operations || has_event_operations {
4828 "InteractionDependencyInvoker"
4829 } else {
4830 "DependencyInvoker"
4831 };
4832 for operation in &contract.operations {
4833 let operation_name = pascal_case(&operation.name);
4834 let request_name = format!("{operation_name}Request");
4835 let response_name = format!("{operation_name}Response");
4836 let error_name = format!("{operation_name}Error");
4837 let request_type = types.type_for(&operation.request, &request_name);
4838 let response_type = types.type_for(&operation.response, &response_name);
4839 let variants = &operation.domain_errors;
4840 let error_type = if variants.is_empty() {
4841 "UnknownDomainError".to_owned()
4842 } else {
4843 let mut values = variants
4844 .iter()
4845 .map(|variant| {
4846 if let Some(payload) = &variant.payload {
4847 let payload_name = format!("{error_name}{}Payload", variant.name);
4848 let payload_type =
4849 types.type_for_non_null(payload.non_null(), &payload_name);
4850 let payload_type = if payload.is_nullable() {
4851 format!("{payload_type} | null")
4852 } else {
4853 payload_type
4854 };
4855 let payload_optional = if variant.payload_required { "" } else { "?" };
4856 format!(
4857 "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
4858 quote_string(&variant.code),
4859 )
4860 } else if variant.structured {
4861 format!("{{ readonly code: {} }}", quote_string(&variant.code))
4862 } else {
4863 quote_string(&variant.code)
4864 }
4865 })
4866 .collect::<Vec<_>>();
4867 values.push("UnknownDomainError".to_owned());
4868 values.join(" | ")
4869 };
4870 let invocation_error_name = format!("{operation_name}InvocationError");
4871 let result_name = format!("{operation_name}Result");
4872 let provider_result_name = if operation.interaction == "stream" {
4873 format!("{operation_name}ProviderResult")
4874 } else {
4875 result_name.clone()
4876 };
4877 let provider_return_type = if operation.interaction == "stream" {
4878 format!("{operation_name}ProviderOutput")
4879 } else {
4880 format!("Promise<{provider_result_name}>")
4881 };
4882 let result_value_type = match operation.interaction.as_str() {
4883 "stream" => format!("StreamSession<{response_type}, {error_name}>"),
4884 "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
4885 _ => response_type.clone(),
4886 };
4887 if operation.interaction == "event" {
4888 errors.push(format!(
4889 "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
4890 ));
4891 } else if operation.interaction == "stream" {
4892 errors.push(format!(
4893 "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}>;"
4894 ));
4895 } else {
4896 errors.push(format!(
4897 "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} }};"
4898 ));
4899 }
4900 codecs.push(generate_typescript_codecs(
4901 &operation.name,
4902 &request_type,
4903 &response_type,
4904 &error_name,
4905 variants,
4906 ));
4907 if operation.interaction == "event" {
4908 codecs.push(generate_typescript_event_codecs(
4909 &operation.name,
4910 &request_type,
4911 ));
4912 }
4913 if matches!(operation.interaction.as_str(), "request" | "stream") {
4914 clients.push(format!(
4915 " {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4916 typescript_property_name(&snake_case(&operation.name)),
4917 ));
4918 providers.push(format!(
4919 " {}(context: InvocationContext, request: {request_type}): {provider_return_type};",
4920 typescript_property_name(&snake_case(&operation.name)),
4921 ));
4922 } else {
4923 clients.push(format!(
4924 " {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4925 typescript_property_name(&snake_case(&operation.name)),
4926 ));
4927 providers.push(format!(
4928 " {}(context: InvocationContext, event: {request_type}): void | Promise<void>;",
4929 typescript_property_name(&snake_case(&operation.name)),
4930 ));
4931 }
4932 if operation.interaction == "request" {
4933 let provider_method = typescript_property_name(&snake_case(&operation.name));
4934 request_dispatch_arms.push(format!(
4935 " 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 }}",
4936 quote_string(&operation.name),
4937 ));
4938 dependency_methods.push(format!(
4939 " 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 }},",
4940 quote_string(&operation.name),
4941 ));
4942 } else if operation.interaction == "stream" {
4943 let provider_method = typescript_property_name(&snake_case(&operation.name));
4944 stream_dispatch_arms.push(format!(
4945 " 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 }}",
4946 quote_string(&operation.name),
4947 ));
4948 dependency_methods.push(format!(
4949 " 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 }},",
4950 quote_string(&operation.name),
4951 ));
4952 } else if operation.interaction == "event" {
4953 let provider_method = typescript_property_name(&snake_case(&operation.name));
4954 event_dispatch_arms.push(format!(
4955 " 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 }}",
4956 quote_string(&operation.name),
4957 ));
4958 dependency_methods.push(format!(
4959 " 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 }},",
4960 quote_string(&operation.name),
4961 ));
4962 }
4963 }
4964 let mut output = String::new();
4965 output.push_str(TYPESCRIPT_HEADER);
4966 output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
4967 writeln!(
4968 output,
4969 "export const CAPABILITY_ID = {};",
4970 quote_string(&contract.capability_id)
4971 )
4972 .expect("writing to a String cannot fail");
4973 writeln!(
4974 output,
4975 "export const DESCRIPTOR_VERSION = {};",
4976 quote_string(&contract.version)
4977 )
4978 .expect("writing to a String cannot fail");
4979 writeln!(
4980 output,
4981 "export const DESCRIPTOR_DIGEST = {};",
4982 quote_string(&contract.descriptor_digest)
4983 )
4984 .expect("writing to a String cannot fail");
4985 writeln!(output, "export const PORTABLE = {};", contract.portable)
4986 .expect("writing to a String cannot fail");
4987 write!(
4988 output,
4989 "export const CROSS_LANE_TRANSFER = {};\n\n",
4990 contract.cross_lane_transfer
4991 )
4992 .expect("writing to a String cannot fail");
4993 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");
4994 if has_stream_operations {
4995 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");
4996 }
4997 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");
4998 if has_event_operations {
4999 output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
5000 }
5001 for declaration in types.declarations {
5002 output.push_str(&declaration);
5003 output.push('\n');
5004 }
5005 for error in errors {
5006 output.push_str(&error);
5007 output.push('\n');
5008 }
5009 for codec in codecs {
5010 output.push_str(&codec);
5011 output.push('\n');
5012 }
5013 write!(
5014 output,
5015 "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
5016 clients.join("\n"),
5017 providers.join("\n")
5018 )
5019 .expect("writing to a String cannot fail");
5020 let dependency_binding = format!("...bind{capability_name}Dependency(), ");
5021 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();
5022 writeln!(
5023 output,
5024 "\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};",
5025 quote_string(&format!("{capability_name}Client")),
5026 )
5027 .expect("writing to a String cannot fail");
5028 write!(
5029 output,
5030 "\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",
5031 request_dispatch_arms.join("\n"),
5032 stream_dispatch_arms.join("\n"),
5033 event_dispatch_arms.join("\n"),
5034 )
5035 .expect("writing to a String cannot fail");
5036 write!(
5037 output,
5038 "\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",
5039 dependency_methods.join("\n"),
5040 )
5041 .expect("writing to a String cannot fail");
5042 output.push_str(
5043 "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
5044 );
5045 output
5046}