1use std::{
10 collections::{BTreeMap, BTreeSet},
11 error::Error,
12 fmt,
13 fmt::Write as _,
14 fs,
15 path::{Path, PathBuf},
16};
17
18use serde_json::{Map, Value};
19
20mod browser;
21mod ir;
22mod rust_debug;
23mod source;
24mod wit;
25
26pub use browser::generate_browser_request_client;
27pub use source::{check_source_snapshot, write_source_snapshot};
28
29use ir::{ContractIr, ErrorVariantIr, FieldIr, ObjectAdditionalIr, OperationIr, TypeIr};
30
31const GENERATED_HEADER: &str = "// @generated by lenso-contract-codegen; do not edit.\n";
32const TYPESCRIPT_HEADER: &str = "/* @generated by lenso-contract-codegen; do not edit. */\n";
33const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
34
35#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
37struct Version(semver::Version);
38
39impl Version {
40 fn parse(value: &str) -> Result<Self, CodegenError> {
41 semver::Version::parse(value)
42 .map(Self)
43 .map_err(|error| CodegenError::InvalidDescriptor {
44 detail: format!("Descriptor version `{value}` is not valid SemVer: {error}"),
45 })
46 }
47
48 const fn major(&self) -> u64 {
49 self.0.major
50 }
51
52 const fn minor(&self) -> u64 {
53 self.0.minor
54 }
55}
56
57fn parse_numeric_component(value: &str) -> Option<u64> {
58 (!value.is_empty() && (value == "0" || !value.starts_with('0')))
59 .then(|| value.parse().ok())
60 .flatten()
61}
62
63#[derive(Clone, Debug)]
64struct Operation {
65 name: String,
66 interaction: String,
67 request_schema: Value,
68 response_schema: Value,
69 domain_error_schema: Value,
70}
71
72fn contract_ir(descriptor: &Descriptor) -> ContractIr {
73 ContractIr {
74 capability_id: descriptor.capability_id.clone(),
75 version: descriptor.version.clone(),
76 portable: descriptor.portable,
77 cross_lane_transfer: descriptor.cross_lane_transfer,
78 operations: descriptor
79 .operations
80 .iter()
81 .map(|operation| OperationIr {
82 name: operation.name.clone(),
83 interaction: operation.interaction.clone(),
84 request: type_ir_from_schema(&operation.request_schema),
85 response: type_ir_from_schema(&operation.response_schema),
86 domain_errors: error_variant_ir_definitions(&operation.domain_error_schema),
87 })
88 .collect(),
89 }
90}
91
92fn type_ir_from_schema(schema: &Value) -> TypeIr {
93 if let Some(types) = schema.get("type").and_then(Value::as_array) {
94 let non_null_types = types
95 .iter()
96 .filter(|schema_type| *schema_type != "null")
97 .cloned()
98 .collect::<Vec<_>>();
99 if non_null_types.is_empty() {
100 return TypeIr::Null;
101 }
102 if non_null_types.len() == 1 {
103 let mut narrowed = schema.as_object().cloned().unwrap_or_default();
104 narrowed.insert("type".to_owned(), non_null_types[0].clone());
105 let base = type_ir_non_null(&Value::Object(narrowed));
106 return if non_null_types.len() == types.len() {
107 base
108 } else {
109 TypeIr::Nullable(Box::new(base))
110 };
111 }
112 }
113 type_ir_non_null(schema)
114}
115
116fn type_ir_non_null(schema: &Value) -> TypeIr {
117 let Some(object) = schema.as_object() else {
118 return TypeIr::Any;
119 };
120 if let Some(values) = object.get("enum").and_then(Value::as_array) {
121 return TypeIr::Enum(
122 values
123 .iter()
124 .filter_map(Value::as_str)
125 .map(ToOwned::to_owned)
126 .collect(),
127 );
128 }
129 if let Some(schema_type) = object.get("type").and_then(Value::as_str) {
130 return match schema_type {
131 "object" => {
132 let required = required_fields(schema);
133 let mut properties = object
134 .get("properties")
135 .and_then(Value::as_object)
136 .into_iter()
137 .flatten()
138 .collect::<Vec<_>>();
139 properties.sort_unstable_by_key(|(name, _)| *name);
144 let fields = properties
145 .into_iter()
146 .map(|(name, schema)| FieldIr {
147 name: name.clone(),
148 required: required.contains(name),
149 sensitive: schema
150 .get("x-lenso-sensitive")
151 .and_then(Value::as_bool)
152 .unwrap_or(false),
153 ty: type_ir_from_schema(schema),
154 })
155 .collect();
156 let additional = match object.get("additionalProperties") {
157 Some(Value::Bool(false)) => ObjectAdditionalIr::Closed,
158 Some(Value::Bool(true)) | None => ObjectAdditionalIr::Any,
159 Some(schema) => {
160 ObjectAdditionalIr::Typed(Box::new(type_ir_from_schema(schema)))
161 }
162 };
163 TypeIr::Object { fields, additional }
164 }
165 "array" => object.get("items").map_or(TypeIr::Any, |items| {
166 TypeIr::Array(Box::new(type_ir_from_schema(items)))
167 }),
168 "string" => match object.get("format").and_then(Value::as_str) {
169 Some("int64") => TypeIr::Int64,
170 Some("uint64") => TypeIr::Uint64,
171 Some("byte") => TypeIr::Bytes,
172 Some("date-time") => TypeIr::Timestamp,
173 Some("duration") => TypeIr::Duration,
174 _ => TypeIr::String,
175 },
176 "integer" => TypeIr::Integer,
177 "number" => TypeIr::Number,
178 "boolean" => TypeIr::Boolean,
179 "null" => TypeIr::Null,
180 _ => TypeIr::Any,
181 };
182 }
183 TypeIr::Any
184}
185
186#[derive(Clone, Debug)]
188pub struct Descriptor {
189 capability_id: String,
190 capability_major: u64,
191 version: String,
192 parsed_version: Version,
193 portable: bool,
194 cross_lane_transfer: bool,
195 operations: Vec<Operation>,
196}
197
198impl Descriptor {
199 #[must_use]
201 pub fn capability_id(&self) -> &str {
202 &self.capability_id
203 }
204
205 #[must_use]
207 pub const fn capability_major(&self) -> u64 {
208 self.capability_major
209 }
210
211 #[must_use]
213 pub fn version(&self) -> &str {
214 &self.version
215 }
216
217 #[must_use]
219 pub const fn portable(&self) -> bool {
220 self.portable
221 }
222
223 #[must_use]
225 pub const fn cross_lane_transfer(&self) -> bool {
226 self.cross_lane_transfer
227 }
228
229 #[must_use]
231 pub fn operation_names(&self) -> Vec<&str> {
232 self.operations
233 .iter()
234 .map(|operation| operation.name.as_str())
235 .collect()
236 }
237}
238
239#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct GeneratedMetadata {
242 pub capability_id: String,
244 pub descriptor_version: String,
246 pub portable: bool,
248 pub cross_lane_transfer: bool,
250}
251
252#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254pub enum ProjectionLanguage {
255 Rust,
257 TypeScript,
259 Wit,
261 RustRuntime,
263}
264
265#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct GeneratedProjection {
268 pub metadata: GeneratedMetadata,
270 pub language: ProjectionLanguage,
272 pub source: String,
274}
275
276#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct GeneratedArtifacts {
279 pub metadata: GeneratedMetadata,
281 pub rust: String,
283 pub typescript: String,
285}
286
287#[derive(Debug)]
289pub enum CodegenError {
290 Io {
292 path: PathBuf,
293 source: std::io::Error,
294 },
295 InvalidDescriptor { detail: String },
297 UnsupportedSchema { path: PathBuf, detail: String },
299 UnsupportedInteraction {
301 operation: String,
302 interaction: String,
303 },
304 UnsupportedWit { detail: String },
306 InvalidPortableValue { path: String, detail: String },
308 GeneratedArtifactDrift { path: PathBuf },
310}
311
312impl fmt::Display for CodegenError {
313 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
314 match self {
315 Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
316 Self::InvalidDescriptor { detail } => {
317 write!(formatter, "invalid Descriptor: {detail}")
318 }
319 Self::UnsupportedSchema { path, detail } => {
320 write!(formatter, "unsupported Schema {}: {detail}", path.display())
321 }
322 Self::UnsupportedInteraction {
323 operation,
324 interaction,
325 } => write!(
326 formatter,
327 "Operation `{operation}` uses unsupported interaction `{interaction}`"
328 ),
329 Self::UnsupportedWit { detail } => {
330 write!(formatter, "unsupported WIT projection: {detail}")
331 }
332 Self::InvalidPortableValue { path, detail } => {
333 write!(formatter, "invalid portable value at `{path}`: {detail}")
334 }
335 Self::GeneratedArtifactDrift { path } => {
336 write!(formatter, "generated artifact is stale: {}", path.display())
337 }
338 }
339 }
340}
341
342impl Error for CodegenError {
343 fn source(&self) -> Option<&(dyn Error + 'static)> {
344 match self {
345 Self::Io { source, .. } => Some(source),
346 _ => None,
347 }
348 }
349}
350
351#[derive(Clone, Debug, Eq, PartialEq)]
354pub enum CompatibilityError {
355 IdentityChanged { from: String, to: String },
357 VersionNotAdvanced { from: String, to: String },
359 BreakingChanges { changes: Vec<String> },
361}
362
363impl fmt::Display for CompatibilityError {
364 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
365 match self {
366 Self::IdentityChanged { from, to } => {
367 write!(
368 formatter,
369 "Capability identity changed from `{from}` to `{to}`"
370 )
371 }
372 Self::VersionNotAdvanced { from, to } => {
373 write!(
374 formatter,
375 "Descriptor version must advance from `{from}` to `{to}`"
376 )
377 }
378 Self::BreakingChanges { changes } => write!(
379 formatter,
380 "breaking Descriptor changes require a new major: {}",
381 changes.join("; ")
382 ),
383 }
384 }
385}
386
387impl Error for CompatibilityError {}
388
389#[allow(clippy::too_many_lines)]
391pub fn load_descriptor(path: &Path) -> Result<Descriptor, CodegenError> {
392 let descriptor_path = canonical_path(path)?;
393 let package_root = descriptor_path
394 .parent()
395 .unwrap_or_else(|| Path::new("."))
396 .to_path_buf();
397 let descriptor_value = read_json(&descriptor_path)?;
398 let object = descriptor_value
399 .as_object()
400 .ok_or_else(|| CodegenError::InvalidDescriptor {
401 detail: "the Descriptor root must be an object".to_owned(),
402 })?;
403
404 let capability_id = required_string(object, "id")?;
405 let (identity, identity_major) = capability_identity(&capability_id)?;
406 let version = required_string(object, "version")?;
407 let parsed_version = Version::parse(&version)?;
408 let portable = object
409 .get("portable")
410 .and_then(Value::as_bool)
411 .unwrap_or(false);
412 let cross_lane_transfer = object
413 .get("cross_lane_transfer")
414 .and_then(Value::as_bool)
415 .unwrap_or(false);
416 let operation_values = object
417 .get("operations")
418 .and_then(Value::as_array)
419 .ok_or_else(|| CodegenError::InvalidDescriptor {
420 detail: "`operations` must be an array".to_owned(),
421 })?;
422 if operation_values.is_empty() {
423 return Err(CodegenError::InvalidDescriptor {
424 detail: "a Descriptor must declare at least one Operation".to_owned(),
425 });
426 }
427
428 let mut operations = Vec::with_capacity(operation_values.len());
429 let mut operation_names = BTreeSet::new();
430 let mut generated_operation_names = BTreeSet::new();
431 let mut generated_type_names = BTreeSet::new();
432 let mut generated_client_method_names =
433 BTreeSet::from(["new".to_owned(), "from_dependencies".to_owned()]);
434 let capability_name = identity
435 .rsplit('.')
436 .next()
437 .map_or_else(|| "Capability".to_owned(), pascal_case);
438 for operation_value in operation_values {
439 let operation =
440 operation_value
441 .as_object()
442 .ok_or_else(|| CodegenError::InvalidDescriptor {
443 detail: "each Operation must be an object".to_owned(),
444 })?;
445 let name = required_string(operation, "name")?;
446 if !is_identifier(&name) || !is_rust_member_name(&name) {
447 return Err(CodegenError::InvalidDescriptor {
448 detail: format!(
449 "Operation name `{name}` must start with a letter or `_` and contain only letters, digits, `_`, or `-`"
450 ),
451 });
452 }
453 if !operation_names.insert(name.clone()) {
454 return Err(CodegenError::InvalidDescriptor {
455 detail: format!("Operation `{name}` is declared more than once"),
456 });
457 }
458 for generated_name in [rust_field_name(&name), pascal_case(&name)] {
459 if !generated_operation_names.insert(generated_name.clone()) {
460 return Err(CodegenError::InvalidDescriptor {
461 detail: format!(
462 "Operation name `{name}` collides after code generation as `{generated_name}`"
463 ),
464 });
465 }
466 }
467 let operation_name = pascal_case(&name);
468 let mut generated_names = vec![
469 format!("{operation_name}Request"),
470 format!("{operation_name}Response"),
471 format!("{operation_name}Error"),
472 format!("{operation_name}InvocationError"),
473 format!("{operation_name}Result"),
474 format!("{capability_name}{operation_name}"),
475 ];
476 generated_names.push(if operation_values.len() == 1 {
477 format!("{capability_name}InvocationError")
478 } else {
479 format!("{capability_name}{operation_name}InvocationError")
480 });
481 for generated_name in generated_names {
482 if !is_generated_type_name(&generated_name)
483 || !generated_type_names.insert(generated_name.clone())
484 {
485 return Err(CodegenError::InvalidDescriptor {
486 detail: format!(
487 "Operation name `{name}` collides after type generation as `{generated_name}`"
488 ),
489 });
490 }
491 }
492 let interaction = required_string(operation, "interaction")?;
493 if !matches!(interaction.as_str(), "request" | "stream" | "event") {
494 return Err(CodegenError::InvalidDescriptor {
495 detail: format!("Operation `{name}` has unsupported interaction `{interaction}`"),
496 });
497 }
498 if matches!(interaction.as_str(), "request" | "stream" | "event") {
499 let client_method_name = rust_field_name(&name);
500 for generated_name in [
501 client_method_name.clone(),
502 format!("{client_method_name}_with_context"),
503 ] {
504 if !generated_client_method_names.insert(generated_name.clone()) {
505 return Err(CodegenError::InvalidDescriptor {
506 detail: format!(
507 "Operation name `{name}` collides with the generated Client API as `{generated_name}`"
508 ),
509 });
510 }
511 }
512 }
513 let request_schema_path =
514 schema_path(&descriptor_path, operation, "request_schema", &name)?;
515 let response_schema_path =
516 schema_path(&descriptor_path, operation, "response_schema", &name)?;
517 let domain_error_schema_path =
518 schema_path(&descriptor_path, operation, "domain_error_schema", &name)?;
519 let mut ref_stack = Vec::new();
520 let request_source = read_json(&request_schema_path)?;
521 let request_schema = resolve_refs(
522 &request_source,
523 &request_source,
524 &request_schema_path,
525 &package_root,
526 &mut ref_stack,
527 )?;
528 validate_schema_profile(&request_schema, &request_schema_path)?;
529 validate_value_generation_schema(&request_schema, &request_schema_path)?;
530 let mut ref_stack = Vec::new();
531 let response_source = read_json(&response_schema_path)?;
532 let response_schema = resolve_refs(
533 &response_source,
534 &response_source,
535 &response_schema_path,
536 &package_root,
537 &mut ref_stack,
538 )?;
539 validate_schema_profile(&response_schema, &response_schema_path)?;
540 validate_value_generation_schema(&response_schema, &response_schema_path)?;
541 let mut ref_stack = Vec::new();
542 let domain_error_source = read_json(&domain_error_schema_path)?;
543 let domain_error_schema = resolve_refs(
544 &domain_error_source,
545 &domain_error_source,
546 &domain_error_schema_path,
547 &package_root,
548 &mut ref_stack,
549 )?;
550 validate_schema_profile(&domain_error_schema, &domain_error_schema_path)?;
551 validate_domain_error_schema(&domain_error_schema, &domain_error_schema_path)?;
552 operations.push(Operation {
553 name,
554 interaction,
555 request_schema,
556 response_schema,
557 domain_error_schema,
558 });
559 }
560 operations.sort_by(|left, right| left.name.cmp(&right.name));
561
562 Ok(Descriptor {
563 capability_id: format!("{identity}@{identity_major}"),
564 capability_major: identity_major,
565 version,
566 parsed_version,
567 portable,
568 cross_lane_transfer,
569 operations,
570 })
571}
572
573fn required_string(object: &Map<String, Value>, key: &str) -> Result<String, CodegenError> {
574 object
575 .get(key)
576 .and_then(Value::as_str)
577 .filter(|value| !value.is_empty())
578 .map(ToOwned::to_owned)
579 .ok_or_else(|| CodegenError::InvalidDescriptor {
580 detail: format!("`{key}` must be a non-empty string"),
581 })
582}
583
584fn capability_identity(value: &str) -> Result<(String, u64), CodegenError> {
585 let Some((identity, major)) = value.rsplit_once('@') else {
586 return Err(CodegenError::InvalidDescriptor {
587 detail: format!("Capability id `{value}` must use `namespace.name@major`"),
588 });
589 };
590 let generated_name = identity
591 .rsplit('.')
592 .next()
593 .map(pascal_case)
594 .unwrap_or_default();
595 if identity.is_empty()
596 || identity.split('.').count() < 2
597 || !identity.split('.').all(is_identifier)
598 || !is_generated_type_name(&generated_name)
599 {
600 return Err(CodegenError::InvalidDescriptor {
601 detail: format!("Capability id `{value}` must use `namespace.name@major`"),
602 });
603 }
604 let Some(major) = parse_numeric_component(major) else {
605 return Err(CodegenError::InvalidDescriptor {
606 detail: format!("Capability id `{value}` has an invalid major"),
607 });
608 };
609 Ok((identity.to_owned(), major))
610}
611
612fn schema_path(
613 descriptor_path: &Path,
614 operation: &Map<String, Value>,
615 key: &str,
616 operation_name: &str,
617) -> Result<PathBuf, CodegenError> {
618 let value = required_string(operation, key).map_err(|error| match error {
619 CodegenError::InvalidDescriptor { detail } => CodegenError::InvalidDescriptor {
620 detail: format!("Operation `{operation_name}`: {detail}"),
621 },
622 other => other,
623 })?;
624 let candidate = descriptor_path
625 .parent()
626 .unwrap_or_else(|| Path::new("."))
627 .join(value);
628 let canonical = canonical_path(&candidate)?;
629 let package_root = canonical_path(descriptor_path.parent().unwrap_or_else(|| Path::new(".")))?;
630 if !canonical.starts_with(&package_root) {
631 return Err(CodegenError::UnsupportedSchema {
632 path: canonical,
633 detail: "Schema paths must remain inside the Descriptor package".to_owned(),
634 });
635 }
636 Ok(canonical)
637}
638
639fn canonical_path(path: &Path) -> Result<PathBuf, CodegenError> {
640 fs::canonicalize(path).map_err(|source| CodegenError::Io {
641 path: path.to_path_buf(),
642 source,
643 })
644}
645
646fn read_json(path: &Path) -> Result<Value, CodegenError> {
647 let source = fs::read_to_string(path).map_err(|source| CodegenError::Io {
648 path: path.to_path_buf(),
649 source,
650 })?;
651 serde_json::from_str(&source).map_err(|error| CodegenError::InvalidDescriptor {
652 detail: format!("{}: {error}", path.display()),
653 })
654}
655
656fn resolve_refs(
657 value: &Value,
658 root: &Value,
659 source_path: &Path,
660 package_root: &Path,
661 stack: &mut Vec<(PathBuf, String)>,
662) -> Result<Value, CodegenError> {
663 match value {
664 Value::Object(object) => {
665 if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
666 let target_value = if reference.starts_with('#') {
667 let fragment = reference.strip_prefix('#').unwrap_or_default();
668 let target_path = source_path.to_path_buf();
669 let key = (target_path.clone(), reference.to_owned());
670 if stack.contains(&key) {
671 return Err(CodegenError::UnsupportedSchema {
672 path: source_path.to_path_buf(),
673 detail: format!("cyclic local Schema `$ref` `{reference}`"),
674 });
675 }
676 let target = json_pointer(root, fragment).ok_or_else(|| {
677 CodegenError::UnsupportedSchema {
678 path: source_path.to_path_buf(),
679 detail: format!(
680 "local JSON Pointer `$ref` `{reference}` was not found"
681 ),
682 }
683 })?;
684 stack.push(key);
685 let resolved = resolve_refs(target, root, source_path, package_root, stack)?;
686 stack.pop();
687 resolved
688 } else {
689 let (reference_path, fragment) =
690 reference.split_once('#').unwrap_or((reference, ""));
691 let target_path = source_path
692 .parent()
693 .unwrap_or_else(|| Path::new("."))
694 .join(reference_path);
695 let target_path = canonical_path(&target_path)?;
696 if !target_path.starts_with(package_root) {
697 return Err(CodegenError::UnsupportedSchema {
698 path: source_path.to_path_buf(),
699 detail: format!(
700 "external Schema `$ref` `{reference}` leaves the Descriptor package"
701 ),
702 });
703 }
704 let key = (target_path.clone(), format!("#{fragment}"));
705 if stack.contains(&key) {
706 return Err(CodegenError::UnsupportedSchema {
707 path: source_path.to_path_buf(),
708 detail: format!(
709 "cyclic Schema `$ref` through {}",
710 target_path.display()
711 ),
712 });
713 }
714 let target_root = read_json(&target_path)?;
715 let target = json_pointer(&target_root, fragment).ok_or_else(|| {
716 CodegenError::UnsupportedSchema {
717 path: source_path.to_path_buf(),
718 detail: format!(
719 "external JSON Pointer `$ref` `{reference}` was not found"
720 ),
721 }
722 })?;
723 stack.push(key);
724 let resolved =
725 resolve_refs(target, &target_root, &target_path, package_root, stack)?;
726 stack.pop();
727 resolved
728 };
729 let mut merged = target_value.as_object().cloned().ok_or_else(|| {
730 CodegenError::UnsupportedSchema {
731 path: source_path.to_path_buf(),
732 detail: "a `$ref` target must resolve to an object Schema".to_owned(),
733 }
734 })?;
735 for (key, child) in object {
736 if key != "$ref" {
737 merged.insert(
738 key.clone(),
739 resolve_refs(child, root, source_path, package_root, stack)?,
740 );
741 }
742 }
743 return Ok(Value::Object(merged));
744 }
745 let mut resolved = Map::new();
746 for (key, child) in object {
747 resolved.insert(
748 key.clone(),
749 resolve_refs(child, root, source_path, package_root, stack)?,
750 );
751 }
752 Ok(Value::Object(resolved))
753 }
754 Value::Array(values) => values
755 .iter()
756 .map(|child| resolve_refs(child, root, source_path, package_root, stack))
757 .collect::<Result<Vec<_>, _>>()
758 .map(Value::Array),
759 _ => Ok(value.clone()),
760 }
761}
762
763fn json_pointer<'a>(root: &'a Value, fragment: &str) -> Option<&'a Value> {
764 if fragment.is_empty() {
765 return Some(root);
766 }
767 let pointer = fragment.strip_prefix('/')?;
768 let mut current = root;
769 for token in pointer.split('/') {
770 let token = token.replace("~1", "/").replace("~0", "~");
771 current = match current {
772 Value::Object(object) => object.get(&token)?,
773 Value::Array(array) => array.get(token.parse::<usize>().ok()?)?,
774 _ => return None,
775 };
776 }
777 Some(current)
778}
779
780fn validate_schema_profile(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
781 let Some(object) = schema.as_object() else {
782 return Err(CodegenError::UnsupportedSchema {
783 path: source_path.to_path_buf(),
784 detail: "a JSON Schema must be an object".to_owned(),
785 });
786 };
787
788 if let Some(format) = object.get("format").and_then(Value::as_str) {
789 if !matches!(
790 format,
791 "int64" | "uint64" | "byte" | "date-time" | "duration"
792 ) {
793 return Err(CodegenError::UnsupportedSchema {
794 path: source_path.to_path_buf(),
795 detail: format!("format `{format}` is outside the portable value profile"),
796 });
797 }
798 if !schema_includes_type(object, "string") {
799 return Err(CodegenError::UnsupportedSchema {
800 path: source_path.to_path_buf(),
801 detail: format!("portable format `{format}` must be attached to a string Schema"),
802 });
803 }
804 }
805
806 if let Some(schema_type) = object.get("type") {
807 match schema_type {
808 Value::String(schema_type) => validate_schema_type(schema_type, object, source_path)?,
809 Value::Array(types) => {
810 if types.is_empty() {
811 return Err(CodegenError::UnsupportedSchema {
812 path: source_path.to_path_buf(),
813 detail: "a Schema type union cannot be empty".to_owned(),
814 });
815 }
816 for schema_type in types {
817 let Some(schema_type) = schema_type.as_str() else {
818 return Err(CodegenError::UnsupportedSchema {
819 path: source_path.to_path_buf(),
820 detail: "Schema type unions must contain strings".to_owned(),
821 });
822 };
823 validate_schema_type(schema_type, object, source_path)?;
824 }
825 }
826 _ => {
827 return Err(CodegenError::UnsupportedSchema {
828 path: source_path.to_path_buf(),
829 detail: "Schema `type` must be a string or array".to_owned(),
830 });
831 }
832 }
833 } else if let Some(alternatives) = object
834 .get("oneOf")
835 .or_else(|| object.get("anyOf"))
836 .and_then(Value::as_array)
837 {
838 for alternative in alternatives {
839 validate_schema_profile(alternative, source_path)?;
840 }
841 } else if !object.contains_key("const") && !object.contains_key("enum") {
842 return Err(CodegenError::UnsupportedSchema {
843 path: source_path.to_path_buf(),
844 detail: "Schema needs a supported `type`, `oneOf`, `anyOf`, `const`, or `enum`"
845 .to_owned(),
846 });
847 }
848
849 if let Some(properties) = object.get("properties") {
850 let Some(properties) = properties.as_object() else {
851 return Err(CodegenError::UnsupportedSchema {
852 path: source_path.to_path_buf(),
853 detail: "Schema `properties` must be an object".to_owned(),
854 });
855 };
856 for property in properties.values() {
857 validate_schema_profile(property, source_path)?;
858 }
859 }
860 if let Some(items) = object.get("items") {
861 validate_schema_profile(items, source_path)?;
862 }
863 if let Some(additional) = object.get("additionalProperties")
864 && !additional.is_boolean()
865 {
866 validate_schema_profile(additional, source_path)?;
867 }
868 if let Some(definitions) = object.get("$defs").and_then(Value::as_object) {
869 for definition in definitions.values() {
870 validate_schema_profile(definition, source_path)?;
871 }
872 }
873 if let Some(alternatives) = object
874 .get("oneOf")
875 .or_else(|| object.get("anyOf"))
876 .and_then(Value::as_array)
877 {
878 for alternative in alternatives {
879 validate_schema_profile(alternative, source_path)?;
880 }
881 }
882 Ok(())
883}
884
885fn validate_schema_type(
886 schema_type: &str,
887 schema: &Map<String, Value>,
888 source_path: &Path,
889) -> Result<(), CodegenError> {
890 if !matches!(
891 schema_type,
892 "object" | "array" | "string" | "integer" | "number" | "boolean" | "null"
893 ) {
894 return Err(CodegenError::UnsupportedSchema {
895 path: source_path.to_path_buf(),
896 detail: format!("Schema type `{schema_type}` is outside the portable profile"),
897 });
898 }
899 if schema_type == "array" && !schema.contains_key("items") {
900 return Err(CodegenError::UnsupportedSchema {
901 path: source_path.to_path_buf(),
902 detail: "array Schemas must declare `items`".to_owned(),
903 });
904 }
905 Ok(())
906}
907
908fn schema_includes_type(schema: &Map<String, Value>, expected: &str) -> bool {
909 match schema.get("type") {
910 Some(Value::String(schema_type)) => schema_type == expected,
911 Some(Value::Array(types)) => types.iter().any(|schema_type| {
912 schema_type
913 .as_str()
914 .is_some_and(|schema_type| schema_type == expected)
915 }),
916 _ => false,
917 }
918}
919
920#[allow(clippy::too_many_lines)]
921fn validate_value_generation_schema(
922 schema: &Value,
923 source_path: &Path,
924) -> Result<(), CodegenError> {
925 let Some(object) = schema.as_object() else {
926 return Err(CodegenError::UnsupportedSchema {
927 path: source_path.to_path_buf(),
928 detail: "generated value Schemas must be objects".to_owned(),
929 });
930 };
931 if object.contains_key("oneOf") || object.contains_key("anyOf") {
932 return Err(CodegenError::UnsupportedSchema {
933 path: source_path.to_path_buf(),
934 detail: "generated value Schemas do not support oneOf/anyOf unions".to_owned(),
935 });
936 }
937 if object.contains_key("allOf") {
938 return Err(CodegenError::UnsupportedSchema {
939 path: source_path.to_path_buf(),
940 detail: "generated value Schemas do not support allOf unions".to_owned(),
941 });
942 }
943 if object.contains_key("const") {
944 return Err(CodegenError::UnsupportedSchema {
945 path: source_path.to_path_buf(),
946 detail: "generated value Schemas do not support const fields".to_owned(),
947 });
948 }
949 if let Some(types) = object.get("type").and_then(Value::as_array) {
950 let non_null = types
951 .iter()
952 .filter(|schema_type| *schema_type != "null")
953 .count();
954 if non_null > 1 {
955 return Err(CodegenError::UnsupportedSchema {
956 path: source_path.to_path_buf(),
957 detail: "generated value Schemas only support nullable type unions".to_owned(),
958 });
959 }
960 let mut narrowed = object.clone();
961 let Some(schema_type) = types.iter().find(|schema_type| *schema_type != "null") else {
962 return Ok(());
963 };
964 narrowed.insert("type".to_owned(), schema_type.clone());
965 return validate_value_generation_schema(&Value::Object(narrowed), source_path);
966 }
967 if let Some(values) = object.get("enum").and_then(Value::as_array) {
968 if values.is_empty() || !values.iter().all(Value::is_string) {
969 return Err(CodegenError::UnsupportedSchema {
970 path: source_path.to_path_buf(),
971 detail: "generated enum values must be non-empty strings".to_owned(),
972 });
973 }
974 let mut enum_names = BTreeSet::new();
975 for value in values.iter().filter_map(Value::as_str) {
976 let name = pascal_case(value);
977 if !is_generated_enum_variant(&name) || !enum_names.insert(name) {
978 return Err(CodegenError::UnsupportedSchema {
979 path: source_path.to_path_buf(),
980 detail: "generated enum values collide after Rust variant generation"
981 .to_owned(),
982 });
983 }
984 }
985 }
986 let Some(schema_type) = object.get("type").and_then(Value::as_str) else {
987 if object.contains_key("enum") {
988 return Ok(());
989 }
990 return Err(CodegenError::UnsupportedSchema {
991 path: source_path.to_path_buf(),
992 detail: "generated value Schemas need a supported type or string enum".to_owned(),
993 });
994 };
995 match schema_type {
996 "object" => {
997 let properties = object
998 .get("properties")
999 .and_then(Value::as_object)
1000 .cloned()
1001 .unwrap_or_default();
1002 let mut rust_names = BTreeSet::new();
1003 let mut nested_type_names = BTreeSet::new();
1004 for (name, property) in &properties {
1005 if !rust_names.insert(rust_field_name(name)) {
1006 return Err(CodegenError::UnsupportedSchema {
1007 path: source_path.to_path_buf(),
1008 detail: format!(
1009 "object properties collide after Rust field normalization near `{name}`"
1010 ),
1011 });
1012 }
1013 if !nested_type_names.insert(pascal_case(name)) {
1014 return Err(CodegenError::UnsupportedSchema {
1015 path: source_path.to_path_buf(),
1016 detail: format!(
1017 "object properties collide after nested type-name generation near `{name}`"
1018 ),
1019 });
1020 }
1021 validate_value_generation_schema(property, source_path)?;
1022 }
1023 match object.get("additionalProperties") {
1024 Some(Value::Bool(false)) => {}
1025 Some(Value::Bool(true)) | None if !properties.is_empty() => {
1026 return Err(CodegenError::UnsupportedSchema {
1027 path: source_path.to_path_buf(),
1028 detail: "open objects with declared properties would lose unknown fields"
1029 .to_owned(),
1030 });
1031 }
1032 Some(additional) if !additional.is_boolean() && !properties.is_empty() => {
1033 return Err(CodegenError::UnsupportedSchema {
1034 path: source_path.to_path_buf(),
1035 detail: "objects cannot combine declared properties with a map payload"
1036 .to_owned(),
1037 });
1038 }
1039 Some(additional) if !additional.is_boolean() => {
1040 validate_value_generation_schema(additional, source_path)?;
1041 }
1042 _ => {}
1043 }
1044 }
1045 "array" => {
1046 let items = object
1047 .get("items")
1048 .ok_or_else(|| CodegenError::UnsupportedSchema {
1049 path: source_path.to_path_buf(),
1050 detail: "generated array Schemas must declare items".to_owned(),
1051 })?;
1052 validate_value_generation_schema(items, source_path)?;
1053 }
1054 "string" | "integer" | "number" | "boolean" | "null" => {}
1055 _ => {
1056 return Err(CodegenError::UnsupportedSchema {
1057 path: source_path.to_path_buf(),
1058 detail: format!("Schema type `{schema_type}` cannot be generated"),
1059 });
1060 }
1061 }
1062 Ok(())
1063}
1064
1065fn validate_domain_error_schema(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1066 if schema
1067 .as_object()
1068 .is_some_and(|object| object.contains_key("allOf"))
1069 {
1070 return Err(CodegenError::UnsupportedSchema {
1071 path: source_path.to_path_buf(),
1072 detail: "Domain Error Schemas do not support allOf unions".to_owned(),
1073 });
1074 }
1075 let variants = schema
1076 .get("oneOf")
1077 .and_then(Value::as_array)
1078 .filter(|variants| !variants.is_empty())
1079 .ok_or_else(|| CodegenError::UnsupportedSchema {
1080 path: source_path.to_path_buf(),
1081 detail: "Domain Error Schemas must be a non-empty oneOf union".to_owned(),
1082 })?;
1083 let mut codes = BTreeSet::new();
1084 let mut names = BTreeSet::from(["Unknown".to_owned()]);
1085 for variant in variants {
1086 if variant
1087 .as_object()
1088 .is_some_and(|object| object.contains_key("allOf"))
1089 {
1090 return Err(CodegenError::UnsupportedSchema {
1091 path: source_path.to_path_buf(),
1092 detail: "Domain Error variants do not support allOf unions".to_owned(),
1093 });
1094 }
1095 if let Some(code) = variant.get("const").and_then(Value::as_str) {
1096 let name = pascal_case(code);
1097 if !codes.insert(code.to_owned())
1098 || !names.insert(name.clone())
1099 || !is_generated_enum_variant(&name)
1100 {
1101 return Err(CodegenError::UnsupportedSchema {
1102 path: source_path.to_path_buf(),
1103 detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1104 });
1105 }
1106 continue;
1107 }
1108 let object = variant
1109 .as_object()
1110 .ok_or_else(|| CodegenError::UnsupportedSchema {
1111 path: source_path.to_path_buf(),
1112 detail: "Domain Error variants must be string consts or objects".to_owned(),
1113 })?;
1114 let properties = object
1115 .get("properties")
1116 .and_then(Value::as_object)
1117 .ok_or_else(|| CodegenError::UnsupportedSchema {
1118 path: source_path.to_path_buf(),
1119 detail: "structured Domain Error variants need properties".to_owned(),
1120 })?;
1121 if object.get("additionalProperties") != Some(&Value::Bool(false))
1122 || !required_fields(variant).contains("code")
1123 || properties
1124 .keys()
1125 .any(|name| name != "code" && name != "payload")
1126 {
1127 return Err(CodegenError::UnsupportedSchema {
1128 path: source_path.to_path_buf(),
1129 detail:
1130 "structured Domain Error objects must explicitly allow only code and payload"
1131 .to_owned(),
1132 });
1133 }
1134 let code = object
1135 .get("properties")
1136 .and_then(Value::as_object)
1137 .and_then(|properties| properties.get("code"))
1138 .and_then(|code| code.get("const"))
1139 .and_then(Value::as_str)
1140 .ok_or_else(|| CodegenError::UnsupportedSchema {
1141 path: source_path.to_path_buf(),
1142 detail: "structured Domain Error variants need a const string code".to_owned(),
1143 })?;
1144 let name = pascal_case(code);
1145 if !codes.insert(code.to_owned())
1146 || !names.insert(name.clone())
1147 || !is_generated_enum_variant(&name)
1148 {
1149 return Err(CodegenError::UnsupportedSchema {
1150 path: source_path.to_path_buf(),
1151 detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1152 });
1153 }
1154 if let Some(payload) = object
1155 .get("properties")
1156 .and_then(Value::as_object)
1157 .and_then(|properties| properties.get("payload"))
1158 {
1159 validate_value_generation_schema(payload, source_path)?;
1160 }
1161 }
1162 Ok(())
1163}
1164
1165fn generation_input(path: &Path) -> Result<(GeneratedMetadata, ContractIr), CodegenError> {
1166 let descriptor = load_descriptor(path)?;
1167 let contract = contract_ir(&descriptor);
1168 let metadata = GeneratedMetadata {
1169 capability_id: descriptor.capability_id.clone(),
1170 descriptor_version: descriptor.version.clone(),
1171 portable: descriptor.portable,
1172 cross_lane_transfer: descriptor.cross_lane_transfer,
1173 };
1174 Ok((metadata, contract))
1175}
1176
1177pub fn generate_projection(
1179 path: &Path,
1180 language: ProjectionLanguage,
1181) -> Result<GeneratedProjection, CodegenError> {
1182 let (metadata, contract) = generation_input(path)?;
1183 let source = match language {
1184 ProjectionLanguage::Rust => generate_rust(&contract),
1185 ProjectionLanguage::TypeScript => generate_typescript(&contract),
1186 ProjectionLanguage::Wit => wit::generate_wit(&contract)?,
1187 ProjectionLanguage::RustRuntime => generate_rust_runtime(&contract)?,
1188 };
1189 Ok(GeneratedProjection {
1190 metadata,
1191 language,
1192 source,
1193 })
1194}
1195
1196pub fn generate(path: &Path) -> Result<GeneratedArtifacts, CodegenError> {
1198 let (metadata, contract) = generation_input(path)?;
1199 Ok(GeneratedArtifacts {
1200 metadata,
1201 rust: generate_rust(&contract),
1202 typescript: generate_typescript(&contract),
1203 })
1204}
1205
1206pub fn write_generated(
1208 descriptor_path: &Path,
1209 rust_path: &Path,
1210 typescript_path: &Path,
1211) -> Result<(), CodegenError> {
1212 let artifacts = generate(descriptor_path)?;
1213 write_artifact(rust_path, &artifacts.rust)?;
1214 write_artifact(typescript_path, &artifacts.typescript)?;
1215 Ok(())
1216}
1217
1218pub fn write_projection(
1220 descriptor_path: &Path,
1221 language: ProjectionLanguage,
1222 output_path: &Path,
1223) -> Result<(), CodegenError> {
1224 let projection = generate_projection(descriptor_path, language)?;
1225 write_artifact(output_path, &projection.source)
1226}
1227
1228pub fn check_generated(
1230 descriptor_path: &Path,
1231 rust_path: &Path,
1232 typescript_path: &Path,
1233) -> Result<(), CodegenError> {
1234 let artifacts = generate(descriptor_path)?;
1235 check_artifact(rust_path, &artifacts.rust)?;
1236 check_artifact(typescript_path, &artifacts.typescript)?;
1237 Ok(())
1238}
1239
1240pub fn check_projection(
1242 descriptor_path: &Path,
1243 language: ProjectionLanguage,
1244 output_path: &Path,
1245) -> Result<(), CodegenError> {
1246 let projection = generate_projection(descriptor_path, language)?;
1247 check_artifact(output_path, &projection.source)
1248}
1249
1250pub fn round_trip_portable_json(value: &Value) -> Result<Value, CodegenError> {
1256 validate_portable_value(value, "$".to_owned())?;
1257 let encoded = serde_json::to_vec(value).expect("JSON values are serializable");
1258 serde_json::from_slice(&encoded).map_err(|error| CodegenError::InvalidPortableValue {
1259 path: "$".to_owned(),
1260 detail: format!("wire JSON could not be decoded: {error}"),
1261 })
1262}
1263
1264pub fn validate_wire_value(schema_path: &Path, value: &Value) -> Result<(), CodegenError> {
1272 let schema_path = canonical_path(schema_path)?;
1273 let package_root = schema_path
1274 .parent()
1275 .unwrap_or_else(|| Path::new("."))
1276 .to_path_buf();
1277 let source = read_json(&schema_path)?;
1278 let mut ref_stack = Vec::new();
1279 let schema = resolve_refs(
1280 &source,
1281 &source,
1282 &schema_path,
1283 &package_root,
1284 &mut ref_stack,
1285 )?;
1286 validate_schema_profile(&schema, &schema_path)?;
1287 validate_wire_value_inner(&schema, value, "$", &schema_path)
1288}
1289
1290fn validate_wire_value_inner(
1291 schema: &Value,
1292 value: &Value,
1293 path: &str,
1294 source_path: &Path,
1295) -> Result<(), CodegenError> {
1296 if let Some(constant) = schema.get("const")
1297 && value != constant
1298 {
1299 return invalid_wire_value(path, "value does not match the Schema const", source_path);
1300 }
1301 if let Some(values) = schema.get("enum").and_then(Value::as_array)
1302 && !values.iter().any(|candidate| candidate == value)
1303 {
1304 return invalid_wire_value(
1305 path,
1306 "value is not included in the Schema enum",
1307 source_path,
1308 );
1309 }
1310
1311 if let Some(alternatives) = schema.get("oneOf").and_then(Value::as_array) {
1312 let matches = alternatives
1313 .iter()
1314 .filter(|alternative| {
1315 validate_wire_value_inner(alternative, value, path, source_path).is_ok()
1316 })
1317 .count();
1318 if matches == 0
1319 && is_open_domain_error_schema(schema)
1320 && is_unknown_domain_error_wire(schema, value)
1321 {
1322 return validate_portable_value(value, path.to_owned());
1323 }
1324 if matches != 1 {
1325 return invalid_wire_value(
1326 path,
1327 "value must match exactly one Schema oneOf alternative",
1328 source_path,
1329 );
1330 }
1331 }
1332 if let Some(alternatives) = schema.get("anyOf").and_then(Value::as_array)
1333 && !alternatives.iter().any(|alternative| {
1334 validate_wire_value_inner(alternative, value, path, source_path).is_ok()
1335 })
1336 {
1337 return invalid_wire_value(
1338 path,
1339 "value must match at least one Schema anyOf alternative",
1340 source_path,
1341 );
1342 }
1343
1344 match schema.get("type") {
1345 Some(Value::String(schema_type)) => {
1346 validate_wire_type(schema, schema_type, value, path, source_path)
1347 }
1348 Some(Value::Array(types)) => {
1349 let mut errors = Vec::new();
1350 for schema_type in types {
1351 let Some(schema_type) = schema_type.as_str() else {
1352 continue;
1353 };
1354 match validate_wire_type(schema, schema_type, value, path, source_path) {
1355 Ok(()) => return Ok(()),
1356 Err(error) => errors.push(error.to_string()),
1357 }
1358 }
1359 invalid_wire_value(
1360 path,
1361 &format!(
1362 "value does not match any Schema type ({})",
1363 errors.join("; ")
1364 ),
1365 source_path,
1366 )
1367 }
1368 Some(_) => invalid_wire_value(path, "Schema type must be a string or array", source_path),
1369 None if schema
1370 .as_object()
1371 .is_some_and(|object| object.contains_key("const") || object.contains_key("enum")) =>
1372 {
1373 Ok(())
1374 }
1375 None => Ok(()),
1376 }
1377}
1378
1379fn is_open_domain_error_schema(schema: &Value) -> bool {
1380 schema
1381 .get("oneOf")
1382 .and_then(Value::as_array)
1383 .is_some_and(|variants| {
1384 !variants.is_empty()
1385 && variants.iter().all(|variant| {
1386 variant.get("const").and_then(Value::as_str).is_some()
1387 || variant
1388 .get("properties")
1389 .and_then(Value::as_object)
1390 .and_then(|properties| properties.get("code"))
1391 .and_then(|code| code.get("const"))
1392 .and_then(Value::as_str)
1393 .is_some()
1394 })
1395 })
1396}
1397
1398fn is_unknown_domain_error_wire(schema: &Value, value: &Value) -> bool {
1399 let known_codes = schema
1400 .get("oneOf")
1401 .and_then(Value::as_array)
1402 .into_iter()
1403 .flatten()
1404 .filter_map(|variant| {
1405 variant.get("const").and_then(Value::as_str).or_else(|| {
1406 variant
1407 .get("properties")
1408 .and_then(Value::as_object)
1409 .and_then(|properties| properties.get("code"))
1410 .and_then(|code| code.get("const"))
1411 .and_then(Value::as_str)
1412 })
1413 })
1414 .collect::<BTreeSet<_>>();
1415 match value {
1416 Value::String(code) => !known_codes.contains(code.as_str()),
1417 Value::Object(object) => object
1418 .get("code")
1419 .and_then(Value::as_str)
1420 .is_some_and(|code| !known_codes.contains(code)),
1421 _ => false,
1422 }
1423}
1424
1425fn validate_wire_type(
1426 schema: &Value,
1427 schema_type: &str,
1428 value: &Value,
1429 path: &str,
1430 source_path: &Path,
1431) -> Result<(), CodegenError> {
1432 match schema_type {
1433 "object" => validate_wire_object(schema, value, path, source_path),
1434 "array" => validate_wire_array(schema, value, path, source_path),
1435 "string" => validate_wire_string(schema, value, path, source_path),
1436 "integer" => {
1437 if !value.is_number()
1438 || value.as_i64().is_none() && value.as_u64().is_none()
1439 || !is_safe_json_integer(value)
1440 {
1441 return invalid_wire_value(path, "expected an integer", source_path);
1442 }
1443 validate_numeric_constraint(schema, value, path, source_path)
1444 }
1445 "number" => {
1446 if value
1447 .as_number()
1448 .is_none_or(|number| !is_safe_json_number(number))
1449 {
1450 return invalid_wire_value(path, "expected a finite number", source_path);
1451 }
1452 validate_numeric_constraint(schema, value, path, source_path)
1453 }
1454 "boolean" => {
1455 if !value.is_boolean() {
1456 return invalid_wire_value(path, "expected a boolean", source_path);
1457 }
1458 Ok(())
1459 }
1460 "null" => {
1461 if !value.is_null() {
1462 return invalid_wire_value(path, "expected null", source_path);
1463 }
1464 Ok(())
1465 }
1466 _ => invalid_wire_value(
1467 path,
1468 "Schema type is outside the portable profile",
1469 source_path,
1470 ),
1471 }
1472}
1473
1474fn is_safe_json_integer(value: &Value) -> bool {
1475 value
1476 .as_i64()
1477 .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
1478 || value
1479 .as_u64()
1480 .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
1481}
1482
1483fn is_safe_json_number(number: &serde_json::Number) -> bool {
1484 number
1485 .as_i64()
1486 .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
1487 || number
1488 .as_u64()
1489 .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
1490 || number.as_f64().is_some_and(|value| {
1491 value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
1492 })
1493}
1494
1495fn validate_wire_object(
1496 schema: &Value,
1497 value: &Value,
1498 path: &str,
1499 source_path: &Path,
1500) -> Result<(), CodegenError> {
1501 let Value::Object(properties) = value else {
1502 return invalid_wire_value(path, "expected an object", source_path);
1503 };
1504 let required = required_fields(schema);
1505 for field in required {
1506 if !properties.contains_key(&field) {
1507 return invalid_wire_value(
1508 &format!("{path}.{field}"),
1509 "required field is missing",
1510 source_path,
1511 );
1512 }
1513 }
1514 let declared = schema
1515 .get("properties")
1516 .and_then(Value::as_object)
1517 .cloned()
1518 .unwrap_or_default();
1519 for (field, field_value) in properties {
1520 if let Some(field_schema) = declared.get(field) {
1521 validate_wire_value_inner(
1522 field_schema,
1523 field_value,
1524 &format!("{path}.{field}"),
1525 source_path,
1526 )?;
1527 continue;
1528 }
1529 match schema.get("additionalProperties") {
1530 Some(Value::Bool(false)) => {
1531 return invalid_wire_value(
1532 &format!("{path}.{field}"),
1533 "additional property is not allowed",
1534 source_path,
1535 );
1536 }
1537 Some(additional_schema) if !additional_schema.is_boolean() => {
1538 validate_wire_value_inner(
1539 additional_schema,
1540 field_value,
1541 &format!("{path}.{field}"),
1542 source_path,
1543 )?;
1544 }
1545 _ => {}
1546 }
1547 }
1548 Ok(())
1549}
1550
1551fn validate_wire_array(
1552 schema: &Value,
1553 value: &Value,
1554 path: &str,
1555 source_path: &Path,
1556) -> Result<(), CodegenError> {
1557 let Value::Array(values) = value else {
1558 return invalid_wire_value(path, "expected an array", source_path);
1559 };
1560 if let Some(items) = schema.get("items") {
1561 for (index, item) in values.iter().enumerate() {
1562 validate_wire_value_inner(items, item, &format!("{path}[{index}]"), source_path)?;
1563 }
1564 }
1565 let length = u64::try_from(values.len()).unwrap_or(u64::MAX);
1566 if let Some(minimum) = schema.get("minItems").and_then(Value::as_u64)
1567 && length < minimum
1568 {
1569 return invalid_wire_value(path, "array has fewer items than minItems", source_path);
1570 }
1571 if let Some(maximum) = schema.get("maxItems").and_then(Value::as_u64)
1572 && length > maximum
1573 {
1574 return invalid_wire_value(path, "array has more items than maxItems", source_path);
1575 }
1576 Ok(())
1577}
1578
1579fn validate_wire_string(
1580 schema: &Value,
1581 value: &Value,
1582 path: &str,
1583 source_path: &Path,
1584) -> Result<(), CodegenError> {
1585 let Value::String(value) = value else {
1586 return invalid_wire_value(path, "expected a string", source_path);
1587 };
1588 validate_string_format(schema, value, path, source_path)?;
1589 let length = u64::try_from(value.chars().count()).unwrap_or(u64::MAX);
1590 if let Some(minimum) = schema.get("minLength").and_then(Value::as_u64)
1591 && length < minimum
1592 {
1593 return invalid_wire_value(path, "string is shorter than minLength", source_path);
1594 }
1595 if let Some(maximum) = schema.get("maxLength").and_then(Value::as_u64)
1596 && length > maximum
1597 {
1598 return invalid_wire_value(path, "string is longer than maxLength", source_path);
1599 }
1600 Ok(())
1601}
1602
1603fn validate_numeric_constraint(
1604 schema: &Value,
1605 value: &Value,
1606 path: &str,
1607 source_path: &Path,
1608) -> Result<(), CodegenError> {
1609 let Some(number) = value.as_f64() else {
1610 return Ok(());
1611 };
1612 if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64)
1613 && number < minimum
1614 {
1615 return invalid_wire_value(path, "number is below minimum", source_path);
1616 }
1617 if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64)
1618 && number > maximum
1619 {
1620 return invalid_wire_value(path, "number is above maximum", source_path);
1621 }
1622 if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64)
1623 && number <= minimum
1624 {
1625 return invalid_wire_value(path, "number is not above exclusiveMinimum", source_path);
1626 }
1627 if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64)
1628 && number >= maximum
1629 {
1630 return invalid_wire_value(path, "number is not below exclusiveMaximum", source_path);
1631 }
1632 Ok(())
1633}
1634
1635fn validate_string_format(
1636 schema: &Value,
1637 value: &str,
1638 path: &str,
1639 source_path: &Path,
1640) -> Result<(), CodegenError> {
1641 let Some(format) = schema.get("format").and_then(Value::as_str) else {
1642 return Ok(());
1643 };
1644 let valid = match format {
1645 "int64" => is_signed_decimal(value),
1646 "uint64" => is_unsigned_decimal(value),
1647 "byte" => is_base64(value),
1648 "date-time" => is_rfc3339(value),
1649 "duration" => is_iso8601_duration(value),
1650 _ => false,
1651 };
1652 if valid {
1653 Ok(())
1654 } else {
1655 invalid_wire_value(
1656 path,
1657 &format!("string does not match portable `{format}` format"),
1658 source_path,
1659 )
1660 }
1661}
1662
1663fn invalid_wire_value<T>(path: &str, detail: &str, _source_path: &Path) -> Result<T, CodegenError> {
1664 Err(CodegenError::InvalidPortableValue {
1665 path: path.to_owned(),
1666 detail: detail.to_owned(),
1667 })
1668}
1669
1670fn is_signed_decimal(value: &str) -> bool {
1671 let digits = value.strip_prefix('-').unwrap_or(value);
1672 !digits.is_empty()
1673 && (digits == "0" || !digits.starts_with('0'))
1674 && digits.chars().all(|character| character.is_ascii_digit())
1675 && value.parse::<i64>().is_ok()
1676}
1677
1678fn is_unsigned_decimal(value: &str) -> bool {
1679 !value.is_empty()
1680 && (value == "0" || !value.starts_with('0'))
1681 && value.chars().all(|character| character.is_ascii_digit())
1682 && value.parse::<u64>().is_ok()
1683}
1684
1685fn is_base64(value: &str) -> bool {
1686 let bytes = value.as_bytes();
1687 if bytes.is_empty() {
1688 return true;
1689 }
1690 if !bytes.len().is_multiple_of(4) {
1691 return false;
1692 }
1693 let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count();
1694 if padding > 2 || bytes[..bytes.len() - padding].contains(&b'=') {
1695 return false;
1696 }
1697 for byte in &bytes[..bytes.len() - padding] {
1698 if base64_digit(*byte).is_none() {
1699 return false;
1700 }
1701 }
1702 let last = &bytes[bytes.len() - 4..];
1703 if padding == 1 {
1704 base64_digit(last[2]).is_some_and(|digit| digit.trailing_zeros() >= 2)
1705 } else if padding == 2 {
1706 base64_digit(last[1]).is_some_and(|digit| digit.trailing_zeros() >= 4)
1707 } else {
1708 base64_digit(last[2]).is_some() && base64_digit(last[3]).is_some()
1709 }
1710}
1711
1712fn base64_digit(byte: u8) -> Option<u8> {
1713 match byte {
1714 b'A'..=b'Z' => Some(byte - b'A'),
1715 b'a'..=b'z' => Some(byte - b'a' + 26),
1716 b'0'..=b'9' => Some(byte - b'0' + 52),
1717 b'+' => Some(62),
1718 b'/' => Some(63),
1719 _ => None,
1720 }
1721}
1722
1723fn is_rfc3339(value: &str) -> bool {
1724 let bytes = value.as_bytes();
1725 if bytes.len() < 20
1726 || !matches!(bytes.get(4), Some(b'-'))
1727 || !matches!(bytes.get(7), Some(b'-'))
1728 || !matches!(bytes.get(10), Some(b'T' | b't'))
1729 || !matches!(bytes.get(13), Some(b':'))
1730 || !matches!(bytes.get(16), Some(b':'))
1731 {
1732 return false;
1733 }
1734 let Some(month) = fixed_digits(bytes, 5, 2) else {
1735 return false;
1736 };
1737 let Some(day) = fixed_digits(bytes, 8, 2) else {
1738 return false;
1739 };
1740 let Some(hour) = fixed_digits(bytes, 11, 2) else {
1741 return false;
1742 };
1743 let Some(minute) = fixed_digits(bytes, 14, 2) else {
1744 return false;
1745 };
1746 let Some(second) = fixed_digits(bytes, 17, 2) else {
1747 return false;
1748 };
1749 let Some(year) = fixed_digits(bytes, 0, 4) else {
1750 return false;
1751 };
1752 let max_day = match month {
1753 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
1754 4 | 6 | 9 | 11 => 30,
1755 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
1756 2 => 28,
1757 _ => return false,
1758 };
1759 if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
1760 return false;
1761 }
1762 let mut index = 19;
1763 if bytes.get(index) == Some(&b'.') {
1764 index += 1;
1765 let start = index;
1766 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
1767 index += 1;
1768 }
1769 if index == start {
1770 return false;
1771 }
1772 }
1773 match bytes.get(index) {
1774 Some(b'Z' | b'z') => index + 1 == bytes.len(),
1775 Some(b'+' | b'-') => {
1776 index += 1;
1777 let Some(offset_hour) = fixed_digits(bytes, index, 2) else {
1778 return false;
1779 };
1780 index += 2;
1781 if bytes.get(index) != Some(&b':') {
1782 return false;
1783 }
1784 index += 1;
1785 let Some(offset_minute) = fixed_digits(bytes, index, 2) else {
1786 return false;
1787 };
1788 offset_hour <= 23 && offset_minute <= 59 && index + 2 == bytes.len()
1789 }
1790 _ => false,
1791 }
1792}
1793
1794fn fixed_digits(bytes: &[u8], start: usize, length: usize) -> Option<u32> {
1795 let slice = bytes.get(start..start + length)?;
1796 slice.iter().try_fold(0_u32, |value, digit| {
1797 digit
1798 .is_ascii_digit()
1799 .then(|| value * 10 + u32::from(digit - b'0'))
1800 })
1801}
1802
1803fn is_iso8601_duration(value: &str) -> bool {
1804 let bytes = value.as_bytes();
1805 let mut index = usize::from(bytes.first() == Some(&b'-'));
1806 if bytes.get(index) != Some(&b'P') {
1807 return false;
1808 }
1809 index += 1;
1810 let mut in_time = false;
1811 let mut saw_component = false;
1812 let mut saw_time = false;
1813 while index < bytes.len() {
1814 if bytes[index] == b'T' {
1815 if in_time || index + 1 == bytes.len() {
1816 return false;
1817 }
1818 in_time = true;
1819 index += 1;
1820 continue;
1821 }
1822 let start = index;
1823 let mut separator_seen = false;
1824 while index < bytes.len()
1825 && (bytes[index].is_ascii_digit() || (!separator_seen && bytes[index] == b'.'))
1826 {
1827 separator_seen |= bytes[index] == b'.';
1828 index += 1;
1829 }
1830 if index == start || bytes.get(index).is_none() {
1831 return false;
1832 }
1833 let unit = bytes[index];
1834 let valid_unit = if in_time {
1835 matches!(unit, b'H' | b'M' | b'S')
1836 } else {
1837 matches!(unit, b'Y' | b'M' | b'W' | b'D')
1838 };
1839 if !valid_unit {
1840 return false;
1841 }
1842 if in_time {
1843 saw_time = true;
1844 }
1845 saw_component = true;
1846 index += 1;
1847 }
1848 saw_component && (!in_time || saw_time)
1849}
1850
1851fn validate_portable_value(value: &Value, path: String) -> Result<(), CodegenError> {
1852 match value {
1853 Value::Number(number) => {
1854 if !is_safe_json_number(number) {
1855 return Err(CodegenError::InvalidPortableValue {
1856 path,
1857 detail: "wide integers must use an explicit decimal-string format".to_owned(),
1858 });
1859 }
1860 }
1861 Value::Array(values) => {
1862 for (index, value) in values.iter().enumerate() {
1863 validate_portable_value(value, format!("{path}[{index}]"))?;
1864 }
1865 }
1866 Value::Object(values) => {
1867 for (key, value) in values {
1868 validate_portable_value(value, format!("{path}.{key}"))?;
1869 }
1870 }
1871 Value::Null | Value::Bool(_) | Value::String(_) => {}
1872 }
1873 Ok(())
1874}
1875
1876fn write_artifact(path: &Path, contents: &str) -> Result<(), CodegenError> {
1877 if let Some(parent) = path.parent() {
1878 fs::create_dir_all(parent).map_err(|source| CodegenError::Io {
1879 path: parent.to_path_buf(),
1880 source,
1881 })?;
1882 }
1883 fs::write(path, contents).map_err(|source| CodegenError::Io {
1884 path: path.to_path_buf(),
1885 source,
1886 })
1887}
1888
1889fn check_artifact(path: &Path, expected: &str) -> Result<(), CodegenError> {
1890 let actual = fs::read_to_string(path).map_err(|source| CodegenError::Io {
1891 path: path.to_path_buf(),
1892 source,
1893 })?;
1894 if actual == expected {
1895 Ok(())
1896 } else {
1897 Err(CodegenError::GeneratedArtifactDrift {
1898 path: path.to_path_buf(),
1899 })
1900 }
1901}
1902
1903pub fn lint_compatibility(old_path: &Path, new_path: &Path) -> Result<bool, CompatibilityError> {
1906 let old = load_descriptor(old_path).map_err(|error| CompatibilityError::BreakingChanges {
1907 changes: vec![error.to_string()],
1908 })?;
1909 let new = load_descriptor(new_path).map_err(|error| CompatibilityError::BreakingChanges {
1910 changes: vec![error.to_string()],
1911 })?;
1912 if old.capability_id != new.capability_id {
1913 return Err(CompatibilityError::IdentityChanged {
1914 from: old.capability_id,
1915 to: new.capability_id,
1916 });
1917 }
1918 if new.parsed_version <= old.parsed_version {
1919 return Err(CompatibilityError::VersionNotAdvanced {
1920 from: old.version,
1921 to: new.version,
1922 });
1923 }
1924 if new.parsed_version.major() != old.parsed_version.major() {
1925 return Err(CompatibilityError::BreakingChanges {
1926 changes: vec!["a breaking change must use a new Capability @major identity".to_owned()],
1927 });
1928 }
1929
1930 let mut changes = Vec::new();
1931 if old.portable != new.portable {
1932 changes.push("Descriptor portability changed".to_owned());
1933 }
1934 if old.cross_lane_transfer && !new.cross_lane_transfer {
1935 changes.push("Descriptor cross-lane transfer support was removed".to_owned());
1936 }
1937 let old_operations: BTreeMap<_, _> = old
1938 .operations
1939 .iter()
1940 .map(|operation| (operation.name.as_str(), operation))
1941 .collect();
1942 let new_operations: BTreeMap<_, _> = new
1943 .operations
1944 .iter()
1945 .map(|operation| (operation.name.as_str(), operation))
1946 .collect();
1947
1948 for (name, old_operation) in &old_operations {
1949 let Some(new_operation) = new_operations.get(name) else {
1950 changes.push(format!("Operation `{name}` was removed"));
1951 continue;
1952 };
1953 if old_operation.interaction != new_operation.interaction {
1954 changes.push(format!("Operation `{name}` changed interaction"));
1955 }
1956 compare_schema(
1957 &old_operation.request_schema,
1958 &new_operation.request_schema,
1959 &format!("Operation `{name}` request"),
1960 &mut changes,
1961 );
1962 compare_schema(
1963 &old_operation.response_schema,
1964 &new_operation.response_schema,
1965 &format!("Operation `{name}` response"),
1966 &mut changes,
1967 );
1968 compare_error_schema(
1969 &old_operation.domain_error_schema,
1970 &new_operation.domain_error_schema,
1971 &format!("Operation `{name}` Domain Error"),
1972 &mut changes,
1973 );
1974 }
1975
1976 if new.parsed_version.minor() == old.parsed_version.minor() {
1977 if contract_signature(&old) != contract_signature(&new) {
1978 changes.push("a patch release changed the observable contract".to_owned());
1979 }
1980 for name in new_operations
1981 .keys()
1982 .filter(|name| !old_operations.contains_key(**name))
1983 {
1984 changes.push(format!("Operation `{name}` was added in a patch release"));
1985 }
1986 }
1987 if new.parsed_version.minor() < old.parsed_version.minor() {
1988 changes.push("Descriptor minor version moved backwards".to_owned());
1989 }
1990
1991 if changes.is_empty() {
1992 Ok(true)
1993 } else {
1994 Err(CompatibilityError::BreakingChanges { changes })
1995 }
1996}
1997
1998fn contract_signature(descriptor: &Descriptor) -> String {
1999 let operations = descriptor
2000 .operations
2001 .iter()
2002 .map(|operation| {
2003 serde_json::json!({
2004 "name": operation.name,
2005 "interaction": operation.interaction,
2006 "request": operation.request_schema,
2007 "response": operation.response_schema,
2008 "domain_error": canonical_domain_error_schema(&operation.domain_error_schema),
2009 })
2010 })
2011 .collect::<Vec<_>>();
2012 canonical_json(&Value::Array(operations))
2013}
2014
2015fn canonical_domain_error_schema(schema: &Value) -> Value {
2016 let Some(variants) = schema.get("oneOf").and_then(Value::as_array) else {
2017 return schema.clone();
2018 };
2019 let mut variants = variants.clone();
2020 variants.sort_by(|left, right| {
2021 error_variant_code(left)
2022 .unwrap_or_default()
2023 .cmp(error_variant_code(right).unwrap_or_default())
2024 });
2025 let mut schema = schema.as_object().cloned().unwrap_or_default();
2026 schema.insert("oneOf".to_owned(), Value::Array(variants));
2027 Value::Object(schema)
2028}
2029
2030fn error_variant_code(variant: &Value) -> Option<&str> {
2031 variant.get("const").and_then(Value::as_str).or_else(|| {
2032 variant
2033 .get("properties")
2034 .and_then(Value::as_object)
2035 .and_then(|properties| properties.get("code"))
2036 .and_then(|code| code.get("const"))
2037 .and_then(Value::as_str)
2038 })
2039}
2040
2041fn compare_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2042 let old_nullable = is_nullable(old);
2043 let new_nullable = is_nullable(new);
2044 if old_nullable != new_nullable {
2045 changes.push(format!("{location} changed nullability"));
2046 return;
2047 }
2048 let old_type = schema_type_name(old);
2049 let new_type = schema_type_name(new);
2050 if old_type != new_type {
2051 changes.push(format!("{location} changed type"));
2052 return;
2053 }
2054 match old_type {
2055 Some("object") => {
2056 let old_properties = old.get("properties").and_then(Value::as_object);
2057 let new_properties = new.get("properties").and_then(Value::as_object);
2058 let (Some(old_properties), Some(new_properties)) = (old_properties, new_properties)
2059 else {
2060 if canonical_json(old) != canonical_json(new) {
2061 changes.push(format!("{location} changed object shape"));
2062 }
2063 return;
2064 };
2065 let old_required = required_fields(old);
2066 let new_required = required_fields(new);
2067 for name in old_properties.keys() {
2068 let Some(new_schema) = new_properties.get(name) else {
2069 changes.push(format!("{location} field `{name}` was removed"));
2070 continue;
2071 };
2072 compare_schema(
2073 &old_properties[name],
2074 new_schema,
2075 &format!("{location} field `{name}`"),
2076 changes,
2077 );
2078 }
2079 for name in new_required.difference(&old_required) {
2080 changes.push(format!("{location} field `{name}` became required"));
2081 }
2082 for name in old_required.difference(&new_required) {
2083 changes.push(format!("{location} field `{name}` became optional"));
2084 }
2085 if old.get("additionalProperties") != new.get("additionalProperties") {
2086 changes.push(format!("{location} changed additional-properties policy"));
2087 }
2088 if schema_constraints_signature(old) != schema_constraints_signature(new) {
2089 changes.push(format!("{location} changed constraints"));
2090 }
2091 }
2092 Some("array") => {
2093 if let (Some(old_items), Some(new_items)) = (old.get("items"), new.get("items")) {
2094 compare_schema(old_items, new_items, &format!("{location} items"), changes);
2095 }
2096 if schema_constraints_signature(old) != schema_constraints_signature(new) {
2097 changes.push(format!("{location} changed constraints"));
2098 }
2099 }
2100 _ => {
2101 if canonical_json(old) != canonical_json(new) {
2102 changes.push(format!("{location} changed format or constraints"));
2103 }
2104 }
2105 }
2106}
2107
2108fn compare_error_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2109 let old_variants = error_variant_values(old);
2110 let new_variants = error_variant_values(new);
2111 if old_variants.is_empty() || new_variants.is_empty() {
2112 if canonical_json(old) != canonical_json(new) {
2113 changes.push(format!("{location} changed shape"));
2114 }
2115 return;
2116 }
2117 for (code, schema) in old_variants {
2118 match new_variants.get(&code) {
2119 None => changes.push(format!("{location} variant `{code}` was removed")),
2120 Some(new_schema) => compare_schema(
2121 &schema,
2122 new_schema,
2123 &format!("{location} variant `{code}`"),
2124 changes,
2125 ),
2126 }
2127 }
2128}
2129
2130fn error_variant_ir_definitions(schema: &Value) -> Vec<ErrorVariantIr> {
2131 let mut variants = schema
2132 .get("oneOf")
2133 .and_then(Value::as_array)
2134 .into_iter()
2135 .flatten()
2136 .filter_map(|variant| {
2137 if let Some(code) = variant.get("const").and_then(Value::as_str) {
2138 return Some(ErrorVariantIr {
2139 code: code.to_owned(),
2140 name: pascal_case(code),
2141 structured: false,
2142 payload: None,
2143 payload_required: false,
2144 });
2145 }
2146 let object = variant.as_object()?;
2147 let code = object
2148 .get("properties")
2149 .and_then(Value::as_object)
2150 .and_then(|properties| properties.get("code"))
2151 .and_then(|code| code.get("const"))
2152 .and_then(Value::as_str)?;
2153 Some(ErrorVariantIr {
2154 code: code.to_owned(),
2155 name: pascal_case(code),
2156 structured: true,
2157 payload: object
2158 .get("properties")
2159 .and_then(Value::as_object)
2160 .and_then(|properties| properties.get("payload"))
2161 .map(type_ir_from_schema),
2162 payload_required: required_fields(variant).contains("payload"),
2163 })
2164 })
2165 .collect::<Vec<_>>();
2166 variants.sort_by(|left, right| left.code.cmp(&right.code));
2167 variants
2168}
2169
2170fn error_variant_values(schema: &Value) -> BTreeMap<String, Value> {
2171 let variants = schema
2172 .get("oneOf")
2173 .and_then(Value::as_array)
2174 .into_iter()
2175 .flatten();
2176 variants
2177 .filter_map(|variant| {
2178 let code = if let Some(code) = variant.get("const").and_then(Value::as_str) {
2179 code.to_owned()
2180 } else {
2181 variant
2182 .get("properties")
2183 .and_then(Value::as_object)
2184 .and_then(|properties| properties.get("code"))
2185 .and_then(|code| code.get("const"))
2186 .and_then(Value::as_str)
2187 .map(ToOwned::to_owned)?
2188 };
2189 Some((code, variant.clone()))
2190 })
2191 .collect()
2192}
2193
2194fn required_fields(schema: &Value) -> BTreeSet<String> {
2195 schema
2196 .get("required")
2197 .and_then(Value::as_array)
2198 .into_iter()
2199 .flatten()
2200 .filter_map(Value::as_str)
2201 .map(ToOwned::to_owned)
2202 .collect()
2203}
2204
2205fn is_nullable(schema: &Value) -> bool {
2206 schema
2207 .get("type")
2208 .and_then(Value::as_array)
2209 .is_some_and(|types| types.iter().any(|value| value == "null"))
2210 || schema
2211 .get("anyOf")
2212 .and_then(Value::as_array)
2213 .is_some_and(|schemas| schemas.iter().any(is_null_schema))
2214 || schema
2215 .get("oneOf")
2216 .and_then(Value::as_array)
2217 .is_some_and(|schemas| schemas.iter().any(is_null_schema))
2218}
2219
2220fn is_null_schema(schema: &Value) -> bool {
2221 schema.get("type").is_some_and(|value| value == "null")
2222 || schema.get("const").is_some_and(Value::is_null)
2223}
2224
2225fn schema_type_name(schema: &Value) -> Option<&str> {
2226 schema.get("type").and_then(|value| match value {
2227 Value::String(value) => Some(value.as_str()),
2228 Value::Array(values) => values
2229 .iter()
2230 .find_map(Value::as_str)
2231 .filter(|value| *value != "null"),
2232 _ => None,
2233 })
2234}
2235
2236fn canonical_json(value: &Value) -> String {
2237 serde_json::to_string(value).expect("JSON values are serializable")
2238}
2239
2240fn schema_constraints_signature(schema: &Value) -> String {
2241 let mut schema = schema.as_object().cloned().unwrap_or_default();
2242 for key in [
2243 "type",
2244 "properties",
2245 "required",
2246 "additionalProperties",
2247 "items",
2248 ] {
2249 schema.remove(key);
2250 }
2251 canonical_json(&Value::Object(schema))
2252}
2253
2254fn is_identifier(value: &str) -> bool {
2255 let mut characters = value.chars();
2256 characters
2257 .next()
2258 .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2259 && characters
2260 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
2261}
2262
2263fn is_rust_member_name(value: &str) -> bool {
2264 !matches!(snake_case(value).as_str(), "self" | "super" | "crate")
2265}
2266
2267fn is_rust_type_identifier(value: &str) -> bool {
2268 let mut characters = value.chars();
2269 let valid = characters
2270 .next()
2271 .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
2272 && characters.all(|character| character.is_ascii_alphanumeric() || character == '_');
2273 valid && value != "Self"
2274}
2275
2276fn is_generated_type_name(value: &str) -> bool {
2277 is_rust_type_identifier(value)
2278 && !matches!(
2279 value,
2280 "Self"
2281 | "String"
2282 | "Int64"
2283 | "Uint64"
2284 | "Bytes"
2285 | "Timestamp"
2286 | "Duration"
2287 | "OptionalValue"
2288 | "UnknownDomainError"
2289 | "InvocationContext"
2290 | "ModuleDependencies"
2291 | "NativeRequestEndpoint"
2292 | "NativeRequestHandle"
2293 | "RequestCapability"
2294 | "RuntimeFailure"
2295 | "LocalBoxFuture"
2296 )
2297}
2298
2299fn is_generated_enum_variant(value: &str) -> bool {
2300 is_rust_type_identifier(value)
2301}
2302
2303fn pascal_case(value: &str) -> String {
2304 let mut output = String::new();
2305 for part in value.split(|character: char| !character.is_ascii_alphanumeric()) {
2306 if part.is_empty() || part.chars().all(char::is_numeric) {
2307 continue;
2308 }
2309 let mut chars = part.chars();
2310 if let Some(first) = chars.next() {
2311 output.extend(first.to_uppercase());
2312 output.push_str(chars.as_str());
2313 }
2314 }
2315 if output.is_empty() {
2316 "Value".to_owned()
2317 } else if output
2318 .chars()
2319 .next()
2320 .is_some_and(|character| character.is_ascii_digit())
2321 {
2322 format!("Value{output}")
2323 } else {
2324 output
2325 }
2326}
2327
2328fn snake_case(value: &str) -> String {
2329 let mut output = String::new();
2330 for (index, character) in value.chars().enumerate() {
2331 if character.is_ascii_alphanumeric() {
2332 if character.is_ascii_uppercase() && index != 0 {
2333 output.push('_');
2334 }
2335 output.push(character.to_ascii_lowercase());
2336 } else if !output.ends_with('_') {
2337 output.push('_');
2338 }
2339 }
2340 let output = output.trim_matches('_').to_owned();
2341 if output.is_empty() {
2342 "value".to_owned()
2343 } else {
2344 output
2345 }
2346}
2347
2348fn screaming_snake_case(value: &str) -> String {
2349 snake_case(value).to_ascii_uppercase()
2350}
2351
2352fn rust_field_name(value: &str) -> String {
2353 let name = snake_case(value);
2354 if matches!(name.as_str(), "self" | "super" | "crate") {
2355 return format!("{name}_");
2356 }
2357 if matches!(
2358 name.as_str(),
2359 "as" | "break"
2360 | "const"
2361 | "continue"
2362 | "crate"
2363 | "else"
2364 | "enum"
2365 | "extern"
2366 | "false"
2367 | "fn"
2368 | "for"
2369 | "if"
2370 | "impl"
2371 | "in"
2372 | "let"
2373 | "loop"
2374 | "match"
2375 | "mod"
2376 | "move"
2377 | "mut"
2378 | "pub"
2379 | "ref"
2380 | "return"
2381 | "self"
2382 | "Self"
2383 | "static"
2384 | "struct"
2385 | "super"
2386 | "trait"
2387 | "true"
2388 | "type"
2389 | "unsafe"
2390 | "use"
2391 | "where"
2392 | "while"
2393 | "async"
2394 | "await"
2395 | "dyn"
2396 ) {
2397 format!("r#{name}")
2398 } else {
2399 name
2400 }
2401}
2402
2403fn quote_string(value: &str) -> String {
2404 serde_json::to_string(value).expect("string is serializable")
2405}
2406
2407fn typescript_property_name(value: &str) -> String {
2408 let mut characters = value.chars();
2409 let valid_identifier = characters
2410 .next()
2411 .is_some_and(|character| character.is_ascii_alphabetic() || matches!(character, '_' | '$'))
2412 && characters
2413 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$'));
2414 if valid_identifier {
2415 value.to_owned()
2416 } else {
2417 quote_string(value)
2418 }
2419}
2420
2421struct RustTypes {
2422 declarations: Vec<String>,
2423 declared: BTreeSet<String>,
2424}
2425
2426impl RustTypes {
2427 fn new() -> Self {
2428 Self {
2429 declarations: Vec::new(),
2430 declared: BTreeSet::new(),
2431 }
2432 }
2433
2434 fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
2435 if !self.declared.insert(name.to_owned()) {
2436 return name.to_owned();
2437 }
2438 let placeholder = self.declarations.len();
2439 self.declarations.push(String::new());
2440 let mut rendered_fields = Vec::new();
2441 for field in fields {
2442 let type_name = self.type_for_non_null(
2443 field.ty.non_null(),
2444 &format!("{name}{}", pascal_case(&field.name)),
2445 );
2446 let field_type = if field.required {
2447 if field.ty.is_nullable() {
2448 format!("Option<{type_name}>")
2449 } else {
2450 type_name
2451 }
2452 } else if field.ty.is_nullable() {
2453 format!("OptionalValue<{type_name}>")
2454 } else {
2455 format!("Option<{type_name}>")
2456 };
2457 let mut attributes = vec![format!(
2458 " #[serde(rename = {})]",
2459 quote_string(&field.name)
2460 )];
2461 if !field.required {
2462 if field.ty.is_nullable() {
2463 attributes.push(" #[serde(default)]".to_owned());
2464 attributes
2465 .push(" #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
2466 attributes.push(
2467 " #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_optional_value\")]"
2468 .to_owned(),
2469 );
2470 } else {
2471 attributes
2472 .push(" #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
2473 }
2474 }
2475 if field.required {
2476 attributes
2477 .push(" #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_required\")]".to_owned());
2478 }
2479 rendered_fields.push(format!(
2480 "{}\n pub {}: {field_type},",
2481 attributes.join("\n"),
2482 rust_field_name(&field.name)
2483 ));
2484 }
2485 let debug_impl = rust_debug::render(
2486 name,
2487 fields.iter().map(|field| {
2488 (
2489 field.name.as_str(),
2490 rust_field_name(&field.name),
2491 field.sensitive,
2492 )
2493 }),
2494 );
2495 let derives = if debug_impl.is_some() {
2496 "Clone, PartialEq, serde::Serialize, serde::Deserialize"
2497 } else {
2498 "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
2499 };
2500 self.declarations[placeholder] = format!(
2501 "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
2502 rendered_fields.join("\n"),
2503 debug_impl = debug_impl.unwrap_or_default(),
2504 );
2505 name.to_owned()
2506 }
2507
2508 fn enum_type(&mut self, name: &str, values: &[String]) -> String {
2509 if !self.declared.insert(name.to_owned()) {
2510 return name.to_owned();
2511 }
2512 let variants = values
2513 .iter()
2514 .map(|value| {
2515 format!(
2516 " #[serde(rename = {})]\n {},",
2517 quote_string(value),
2518 pascal_case(value)
2519 )
2520 })
2521 .collect::<Vec<_>>();
2522 self.declarations.push(format!(
2523 "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
2524 variants.join("\n")
2525 ));
2526 name.to_owned()
2527 }
2528
2529 fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2530 let base = self.type_for_non_null(ty.non_null(), nested_name);
2531 if ty.is_nullable() {
2532 format!("Option<{base}>")
2533 } else {
2534 base
2535 }
2536 }
2537
2538 fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2539 match ty {
2540 TypeIr::Any => "serde_json::Value".to_owned(),
2541 TypeIr::String => "String".to_owned(),
2542 TypeIr::Enum(values) => self.enum_type(nested_name, values),
2543 TypeIr::Int64 => "Int64".to_owned(),
2544 TypeIr::Uint64 => "Uint64".to_owned(),
2545 TypeIr::Bytes => "Bytes".to_owned(),
2546 TypeIr::Timestamp => "Timestamp".to_owned(),
2547 TypeIr::Duration => "Duration".to_owned(),
2548 TypeIr::Integer => "i64".to_owned(),
2549 TypeIr::Number => "f64".to_owned(),
2550 TypeIr::Boolean => "bool".to_owned(),
2551 TypeIr::Null => "()".to_owned(),
2552 TypeIr::Array(items) => {
2553 format!(
2554 "Vec<{}>",
2555 self.type_for(items, &format!("{nested_name}Item"))
2556 )
2557 }
2558 TypeIr::Object { fields, additional } => {
2559 if fields.is_empty() {
2560 match additional {
2561 ObjectAdditionalIr::Closed => self.object(nested_name, fields),
2562 ObjectAdditionalIr::Any => {
2563 "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
2564 }
2565 ObjectAdditionalIr::Typed(values) => format!(
2566 "std::collections::BTreeMap<String, {}>",
2567 self.type_for(values, &format!("{nested_name}Value"))
2568 ),
2569 }
2570 } else {
2571 self.object(nested_name, fields)
2572 }
2573 }
2574 TypeIr::Nullable(inner) => {
2575 format!("Option<{}>", self.type_for_non_null(inner, nested_name))
2576 }
2577 }
2578 }
2579}
2580
2581struct TypeScriptTypes {
2582 declarations: Vec<String>,
2583 declared: BTreeSet<String>,
2584}
2585
2586impl TypeScriptTypes {
2587 fn new() -> Self {
2588 Self {
2589 declarations: Vec::new(),
2590 declared: BTreeSet::new(),
2591 }
2592 }
2593
2594 fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
2595 if !self.declared.insert(name.to_owned()) {
2596 return name.to_owned();
2597 }
2598 let placeholder = self.declarations.len();
2599 self.declarations.push(String::new());
2600 let mut rendered_fields = Vec::new();
2601 for field in fields {
2602 let type_name = self.type_for_non_null(
2603 field.ty.non_null(),
2604 &format!("{name}{}", pascal_case(&field.name)),
2605 );
2606 let optional = if field.required { "" } else { "?" };
2607 let field_type = if field.ty.is_nullable() {
2608 format!("{type_name} | null")
2609 } else {
2610 type_name
2611 };
2612 rendered_fields.push(format!(
2613 " {}{optional}: {field_type};",
2614 typescript_property_name(&field.name)
2615 ));
2616 }
2617 self.declarations[placeholder] = format!(
2618 "export interface {name} {{\n{}\n}}\n",
2619 rendered_fields.join("\n")
2620 );
2621 name.to_owned()
2622 }
2623
2624 fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2625 let base = self.type_for_non_null(ty.non_null(), nested_name);
2626 if ty.is_nullable() {
2627 format!("{base} | null")
2628 } else {
2629 base
2630 }
2631 }
2632
2633 fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
2634 match ty {
2635 TypeIr::Any => "unknown".to_owned(),
2636 TypeIr::String => "string".to_owned(),
2637 TypeIr::Int64 => "Int64".to_owned(),
2638 TypeIr::Uint64 => "Uint64".to_owned(),
2639 TypeIr::Bytes => "Bytes".to_owned(),
2640 TypeIr::Timestamp => "Timestamp".to_owned(),
2641 TypeIr::Duration => "Duration".to_owned(),
2642 TypeIr::Integer | TypeIr::Number => "number".to_owned(),
2643 TypeIr::Boolean => "boolean".to_owned(),
2644 TypeIr::Null => "null".to_owned(),
2645 TypeIr::Enum(values) => values
2646 .iter()
2647 .map(|value| quote_string(value))
2648 .collect::<Vec<_>>()
2649 .join(" | "),
2650 TypeIr::Array(items) => {
2651 format!(
2652 "Array<{}>",
2653 self.type_for(items, &format!("{nested_name}Item"))
2654 )
2655 }
2656 TypeIr::Object { fields, additional } => {
2657 if fields.is_empty() {
2658 match additional {
2659 ObjectAdditionalIr::Closed => self.object(nested_name, fields),
2660 ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
2661 ObjectAdditionalIr::Typed(values) => format!(
2662 "Record<string, {}>",
2663 self.type_for(values, &format!("{nested_name}Value"))
2664 ),
2665 }
2666 } else {
2667 self.object(nested_name, fields)
2668 }
2669 }
2670 TypeIr::Nullable(inner) => {
2671 format!("{} | null", self.type_for_non_null(inner, nested_name))
2672 }
2673 }
2674 }
2675}
2676
2677#[allow(clippy::too_many_lines)]
2678fn generate_rust(contract: &ContractIr) -> String {
2679 let capability_name = pascal_case(
2680 contract
2681 .capability_id
2682 .split('@')
2683 .next()
2684 .and_then(|identity| identity.rsplit('.').next())
2685 .unwrap_or("Capability"),
2686 );
2687 let capability_const = screaming_snake_case(&capability_name);
2688 let native_support_name = format!("__LensoNativeSupport{capability_name}");
2689 let mut types = RustTypes::new();
2690 let mut operation_rows = Vec::new();
2691 let mut stream_operation_rows = Vec::new();
2692 let mut event_operation_rows = Vec::new();
2693 let mut operation_markers = Vec::new();
2694 let mut provider_methods = Vec::new();
2695 let mut provider_lowering_methods = Vec::new();
2696 let mut provider_result_conversions = Vec::new();
2697 let mut endpoint_arms = Vec::new();
2698 let mut stream_endpoint_arms = Vec::new();
2699 let mut event_endpoint_arms = Vec::new();
2700 let mut client_fields = Vec::new();
2701 let mut client_initializers = Vec::new();
2702 let mut client_methods = Vec::new();
2703 let mut invocation_errors = Vec::new();
2704 let mut error_codecs = Vec::new();
2705 let mut wire_codecs = Vec::new();
2706
2707 for operation in &contract.operations {
2708 let operation_name = pascal_case(&operation.name);
2709 let request_name = format!("{operation_name}Request");
2710 let response_name = format!("{operation_name}Response");
2711 let error_name = format!("{operation_name}Error");
2712 let marker_name = if contract.operations.len() == 1 {
2713 capability_name.clone()
2714 } else {
2715 format!("{capability_name}{operation_name}")
2716 };
2717 let request_type = types.type_for(&operation.request, &request_name);
2718 let response_type = types.type_for(&operation.response, &response_name);
2719 let known_errors = &operation.domain_errors;
2720 let error_definition = if known_errors.is_empty() {
2721 format!(
2722 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n Unknown(UnknownDomainError),\n}}\n"
2723 )
2724 } else {
2725 let variants = known_errors
2726 .iter()
2727 .map(|variant| {
2728 if let Some(payload) = &variant.payload {
2729 let payload_name = format!("{error_name}{}Payload", variant.name);
2730 let payload_type =
2731 types.type_for_non_null(payload.non_null(), &payload_name);
2732 let payload_type = if variant.payload_required {
2733 if payload.is_nullable() {
2734 format!("Option<{payload_type}>")
2735 } else {
2736 payload_type
2737 }
2738 } else if payload.is_nullable() {
2739 format!("OptionalValue<{payload_type}>")
2740 } else {
2741 format!("Option<{payload_type}>")
2742 };
2743 format!(" {} {{ payload: {payload_type} }},", variant.name)
2744 } else {
2745 format!(" {},", variant.name)
2746 }
2747 })
2748 .chain(std::iter::once(
2749 " Unknown(UnknownDomainError),".to_owned(),
2750 ))
2751 .collect::<Vec<_>>();
2752 format!(
2753 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
2754 variants.join("\n")
2755 )
2756 };
2757 types.declarations.push(error_definition);
2758 error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
2759 wire_codecs.push(generate_rust_wire_codecs(
2760 &operation.name,
2761 &request_type,
2762 &response_type,
2763 &error_name,
2764 ));
2765 if operation.interaction == "event" {
2766 wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
2767 }
2768 let operation_const = screaming_snake_case(&operation.name);
2769 operation_markers.push(match operation.interaction.as_str() {
2770 "request" => {
2771 let provider_method = rust_field_name(&operation.name);
2772 format!(
2773 "#[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"
2774 )
2775 }
2776 "stream" => format!(
2777 "#[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"
2778 ),
2779 "event" => format!(
2780 "#[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"
2781 ),
2782 _ => unreachable!("Descriptor validation restricts interactions"),
2783 });
2784 if operation.interaction == "request" {
2785 let invocation_error_name = if contract.operations.len() == 1 {
2786 format!("{capability_name}InvocationError")
2787 } else {
2788 format!("{capability_name}{operation_name}InvocationError")
2789 };
2790 operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
2791 provider_methods.push(format!(
2792 " fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
2793 rust_field_name(&operation.name),
2794 ));
2795 let field = rust_field_name(&operation.name);
2796 let conversion = format!("__LensoInto{capability_name}{operation_name}Result");
2797 provider_result_conversions.push(format!(
2798 "#[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<{response_type}, lenso_module_authoring::ModuleError<{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_module_authoring::ModuleError::Domain(error)) => Ok(Err(error)),\n Err(lenso_module_authoring::ModuleError::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"
2799 ));
2800 provider_lowering_methods.push(format!(
2801 " fn {field}(&self, context: {native_support_name}::InvocationContext, request: $crate::{request_type}) -> {native_support_name}::NativeRequestFuture<$crate::{marker_name}> {{\n let module = self.clone();\n ::std::boxed::Box::pin(async move {{\n let result = <$module>::{field}(&module, context, request).await;\n $crate::{conversion}::__lenso_into_result(result)\n }})\n }}"
2802 ));
2803 endpoint_arms.push(format!(
2804 " {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 }}",
2805 rust_field_name(&operation.name),
2806 ));
2807 let field = rust_field_name(&operation.name);
2808 client_fields.push(format!(" {field}: NativeRequestHandle<{marker_name}>,"));
2809 client_initializers.push(format!(
2810 " {field}: dependencies.one::<{marker_name}>()?,"
2811 ));
2812 client_methods.push(format!(
2813 " 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 }}",
2814 screaming_snake_case(&operation.name),
2815 screaming_snake_case(&operation.name),
2816 ));
2817 invocation_errors.push(format!(
2818 "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n Domain({error_name}),\n Runtime(RuntimeFailure),\n}}\n"
2819 ));
2820 } else if operation.interaction == "stream" {
2821 let invocation_error_name = if contract.operations.len() == 1 {
2822 format!("{capability_name}InvocationError")
2823 } else {
2824 format!("{capability_name}{operation_name}InvocationError")
2825 };
2826 stream_operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
2827 provider_methods.push(format!(
2828 " fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
2829 rust_field_name(&operation.name)
2830 ));
2831 let field = rust_field_name(&operation.name);
2832 let conversion = format!("__LensoInto{capability_name}{operation_name}StreamResult");
2833 provider_result_conversions.push(format!(
2834 "#[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_module_authoring::ModuleError<{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_module_authoring::ModuleError::Domain(error)) => Err({invocation_error_name}::Domain(error)),\n Err(lenso_module_authoring::ModuleError::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"
2835 ));
2836 provider_lowering_methods.push(format!(
2837 " 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 module = self.clone();\n ::std::boxed::Box::pin(async move {{\n let result = <$module>::{field}(&module, context, request).await;\n $crate::{conversion}::__lenso_into_result(result)\n }})\n }}"
2838 ));
2839 stream_endpoint_arms.push(format!(
2840 " {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 }}",
2841 rust_field_name(&operation.name),
2842 ));
2843 let field = rust_field_name(&operation.name);
2844 client_fields.push(format!(" {field}: NativeStreamHandle<{marker_name}>,"));
2845 client_initializers.push(format!(
2846 " {field}: dependencies.one_stream::<{marker_name}>()?,"
2847 ));
2848 client_methods.push(format!(
2849 " 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 }}"
2850 ));
2851 invocation_errors.push(format!(
2852 "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n Domain({error_name}),\n Runtime(RuntimeFailure),\n}}\n"
2853 ));
2854 } else {
2855 event_operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
2856 provider_methods.push(format!(
2857 " fn {}(&self, context: InvocationContext, event: {request_type}) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;",
2858 rust_field_name(&operation.name)
2859 ));
2860 let field = rust_field_name(&operation.name);
2861 let conversion = format!("__LensoInto{capability_name}{operation_name}EventResult");
2862 provider_result_conversions.push(format!(
2863 "#[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"
2864 ));
2865 provider_lowering_methods.push(format!(
2866 " fn {field}(&self, context: {native_support_name}::InvocationContext, event: $crate::{request_type}) -> {native_support_name}::LocalBoxFuture<'static, Result<(), {native_support_name}::RuntimeFailure>> {{\n let module = self.clone();\n ::std::boxed::Box::pin(async move {{\n let result = <$module>::{field}(&module, context, event).await;\n $crate::{conversion}::__lenso_into_result(result)\n }})\n }}"
2867 ));
2868 event_endpoint_arms.push(format!(
2869 " {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 }}",
2870 rust_field_name(&operation.name),
2871 ));
2872 let field = rust_field_name(&operation.name);
2873 client_fields.push(format!(" {field}: NativeEventHandle<{marker_name}>,"));
2874 client_initializers.push(format!(
2875 " {field}: dependencies.many_event::<{marker_name}>()?,"
2876 ));
2877 client_methods.push(format!(
2878 " 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 }}"
2879 ));
2880 }
2881 }
2882
2883 let request_endpoint_impl = if operation_rows.is_empty() {
2884 String::new()
2885 } else {
2886 format!(
2887 "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",
2888 operation_rows.concat(),
2889 endpoint_arms.join(",\n")
2890 )
2891 };
2892 let stream_endpoint_impl = if stream_operation_rows.is_empty() {
2893 String::new()
2894 } else {
2895 format!(
2896 "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",
2897 stream_operation_rows.concat(),
2898 stream_endpoint_arms.join(",\n")
2899 )
2900 };
2901 let event_endpoint_impl = if event_operation_rows.is_empty() {
2902 String::new()
2903 } else {
2904 format!(
2905 "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",
2906 event_operation_rows.concat(),
2907 event_endpoint_arms.join(",\n")
2908 )
2909 };
2910 let mut output = String::new();
2911 output.push_str(GENERATED_HEADER);
2912 let has_request_operations = !operation_rows.is_empty();
2913 let has_stream_operations = !stream_operation_rows.is_empty();
2914 let has_event_operations = !event_operation_rows.is_empty();
2915 let mut kernel_imports = vec!["InvocationContext", "ModuleDependencies", "RuntimeFailure"];
2916 if has_request_operations {
2917 kernel_imports.extend([
2918 "NativeRequestFuture",
2919 "NativeRequestEndpoint",
2920 "NativeRequestHandle",
2921 "RequestCapability",
2922 ]);
2923 }
2924 if has_stream_operations {
2925 kernel_imports.extend([
2926 "NativeStream",
2927 "NativeStreamEndpoint",
2928 "NativeStreamHandle",
2929 "NativeStreamSession",
2930 "StreamCapability",
2931 "StreamEvent",
2932 ]);
2933 }
2934 if has_event_operations {
2935 kernel_imports.extend([
2936 "EventCapability",
2937 "EventPublishResult",
2938 "NativeEventEndpoint",
2939 "NativeEventHandle",
2940 ]);
2941 }
2942 kernel_imports.sort_unstable();
2943 writeln!(
2944 output,
2945 "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
2946 kernel_imports.join(", ")
2947 )
2948 .expect("writing to a String cannot fail");
2949 output.push_str("use lenso_module_authoring::CapabilityClient;\n");
2950 writeln!(
2951 output,
2952 "pub const CAPABILITY_ID: &str = {};",
2953 quote_string(&contract.capability_id)
2954 )
2955 .expect("writing to a String cannot fail");
2956 writeln!(
2957 output,
2958 "pub const DESCRIPTOR_VERSION: &str = {};",
2959 quote_string(&contract.version)
2960 )
2961 .expect("writing to a String cannot fail");
2962 writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
2963 .expect("writing to a String cannot fail");
2964 writeln!(
2965 output,
2966 "pub const CROSS_LANE_TRANSFER: bool = {};",
2967 contract.cross_lane_transfer
2968 )
2969 .expect("writing to a String cannot fail");
2970 writeln!(
2971 output,
2972 "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
2973 )
2974 .expect("writing to a String cannot fail");
2975 write!(
2976 output,
2977 "pub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\n\n"
2978 )
2979 .expect("writing to a String cannot fail");
2980
2981 let capability_macro_name = snake_case(&capability_name);
2982 let client_macro_name = snake_case(&format!("{capability_name}Client"));
2983 let operations = contract
2984 .operations
2985 .iter()
2986 .map(|operation| Value::String(operation.name.clone()))
2987 .collect::<Vec<_>>();
2988 let operation_kinds = contract
2989 .operations
2990 .iter()
2991 .filter(|operation| operation.interaction != "request")
2992 .map(|operation| {
2993 (
2994 operation.name.clone(),
2995 Value::String(operation.interaction.clone()),
2996 )
2997 })
2998 .collect::<Map<_, _>>();
2999 let provided_fragment = canonical_json(&serde_json::json!({
3000 "capability_id": contract.capability_id,
3001 "descriptor_version": contract.version,
3002 "operations": operations,
3003 "operation_kinds": operation_kinds,
3004 "default_admission": {
3005 "queue_capacity": 0,
3006 "max_concurrency": 1
3007 },
3008 "operation_admissions": {},
3009 "event_admission": null,
3010 "cross_lane_transfer": contract.cross_lane_transfer
3011 }));
3012 let required_fragment = canonical_json(&serde_json::json!({
3013 "capability_id": contract.capability_id,
3014 "descriptor_version": contract.version,
3015 "cardinality": "one"
3016 }));
3017 writeln!(
3018 output,
3019 "#[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",
3020 quote_string(&provided_fragment),
3021 quote_string(&required_fragment),
3022 )
3023 .expect("writing to a String cannot fail");
3024 for operation in &contract.operations {
3025 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
3026 writeln!(
3027 output,
3028 "pub const {operation_const}: &str = {};",
3029 quote_string(&operation.name)
3030 )
3031 .expect("writing to a String cannot fail");
3032 }
3033 writeln!(
3034 output,
3035 "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
3036 rust_runtime_types(contract).join(", ")
3037 )
3038 .expect("writing to a String cannot fail");
3039 for declaration in types.declarations {
3040 output.push_str(&declaration);
3041 output.push('\n');
3042 }
3043 for marker in operation_markers {
3044 output.push_str(&marker);
3045 output.push('\n');
3046 }
3047 for codec in error_codecs {
3048 output.push_str(&codec);
3049 output.push('\n');
3050 }
3051 for codec in wire_codecs {
3052 output.push_str(&codec);
3053 output.push('\n');
3054 }
3055 for conversion in provider_result_conversions {
3056 output.push_str(&conversion);
3057 output.push('\n');
3058 }
3059 write!(
3060 output,
3061 "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
3062 provider_methods.join("\n")
3063 )
3064 .expect("writing to a String cannot fail");
3065 writeln!(
3066 output,
3067 "#[doc(hidden)]\n#[macro_export]\nmacro_rules! __lenso_native_lower_{capability_macro_name} {{\n ($module:ty, $support:path) => {{\n use $support as {native_support_name};\n impl $crate::{capability_name}Provider for $module {{\n{}\n }}\n }};\n}}\n",
3068 provider_lowering_methods.join("\n")
3069 )
3070 .expect("writing to a String cannot fail");
3071 if has_request_operations {
3072 write!(
3073 output,
3074 "#[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"
3075 )
3076 .expect("writing to a String cannot fail");
3077 } else {
3078 write!(
3079 output,
3080 "#[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"
3081 )
3082 .expect("writing to a String cannot fail");
3083 }
3084 output.push_str(&request_endpoint_impl);
3085 output.push_str(&stream_endpoint_impl);
3086 output.push_str(&event_endpoint_impl);
3087 let request_endpoint_value = has_request_operations.then_some(
3088 "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeRequestEndpoint>",
3089 );
3090 let stream_endpoint_value = has_stream_operations.then_some(
3091 "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeStreamEndpoint>",
3092 );
3093 let event_endpoint_value = has_event_operations
3094 .then_some("endpoint as ::std::rc::Rc<dyn __LensoNativeSupport::NativeEventEndpoint>");
3095 writeln!(
3096 output,
3097 "#[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::NativeModuleInstance::with_all_endpoints(\n request_endpoints,\n stream_endpoints,\n event_endpoints,\n $lifecycle,\n )\n }}}};\n}}\n",
3098 request_endpoint_value.unwrap_or_default(),
3099 stream_endpoint_value.unwrap_or_default(),
3100 event_endpoint_value.unwrap_or_default(),
3101 )
3102 .expect("writing to a String cannot fail");
3103 let new_method = if contract.operations.len() == 1 {
3104 let field = rust_field_name(&contract.operations[0].name);
3105 let marker = &capability_name;
3106 match contract.operations[0].interaction.as_str() {
3107 "request" => format!(
3108 " pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
3109 ),
3110 "stream" => format!(
3111 " pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
3112 ),
3113 "event" => format!(
3114 " pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
3115 ),
3116 _ => unreachable!("Descriptor validation restricts interactions"),
3117 }
3118 } else {
3119 String::new()
3120 };
3121 write!(
3122 output,
3123 "#[derive(Debug)]\npub struct {capability_name}Client {{\n{}\n}}\nimpl {capability_name}Client {{\n{} pub fn from_dependencies(dependencies: &ModuleDependencies) -> Result<Self, RuntimeFailure> {{\n <Self as CapabilityClient>::from_dependencies(dependencies)\n }}\n\n{}\n}}\n\nimpl CapabilityClient for {capability_name}Client {{\n type Dependencies = ModuleDependencies;\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: &ModuleDependencies) -> Result<Self, RuntimeFailure> {{\n Ok(Self {{\n{}\n }})\n }}\n\n fn already_connected() -> RuntimeFailure {{\n RuntimeFailure::ModuleFailure {{\n detail: format!(\"Capability Port {{CAPABILITY_ID}} was connected more than once\"),\n }}\n }}\n}}\n\n",
3124 client_fields.join("\n"),
3125 new_method,
3126 client_methods.join("\n\n"),
3127 client_initializers.join("\n")
3128 )
3129 .expect("writing to a String cannot fail");
3130 for error in invocation_errors {
3131 output.push_str(&error);
3132 }
3133 format!("{}\n", output.trim_end())
3134}
3135
3136fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
3137 let mut types = BTreeSet::from(["UnknownDomainError"]);
3138 for operation in &contract.operations {
3139 collect_rust_runtime_types(&operation.request, &mut types);
3140 collect_rust_runtime_types(&operation.response, &mut types);
3141 for error in &operation.domain_errors {
3142 if let Some(payload) = &error.payload {
3143 if !error.payload_required && payload.is_nullable() {
3144 types.insert("OptionalValue");
3145 }
3146 collect_rust_runtime_types(payload, &mut types);
3147 }
3148 }
3149 }
3150 types.into_iter().collect()
3151}
3152
3153fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
3154 match ty {
3155 TypeIr::Int64 => {
3156 types.insert("Int64");
3157 }
3158 TypeIr::Uint64 => {
3159 types.insert("Uint64");
3160 }
3161 TypeIr::Bytes => {
3162 types.insert("Bytes");
3163 }
3164 TypeIr::Timestamp => {
3165 types.insert("Timestamp");
3166 }
3167 TypeIr::Duration => {
3168 types.insert("Duration");
3169 }
3170 TypeIr::Array(item) | TypeIr::Nullable(item) => {
3171 collect_rust_runtime_types(item, types);
3172 }
3173 TypeIr::Object { fields, additional } => {
3174 for field in fields {
3175 if !field.required && field.ty.is_nullable() {
3176 types.insert("OptionalValue");
3177 }
3178 collect_rust_runtime_types(&field.ty, types);
3179 }
3180 if let ObjectAdditionalIr::Typed(value) = additional {
3181 collect_rust_runtime_types(value, types);
3182 }
3183 }
3184 TypeIr::Any
3185 | TypeIr::String
3186 | TypeIr::Integer
3187 | TypeIr::Number
3188 | TypeIr::Boolean
3189 | TypeIr::Null
3190 | TypeIr::Enum(_) => {}
3191 }
3192}
3193
3194#[allow(clippy::too_many_lines)]
3195fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
3196 let mut output = String::new();
3197 writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
3198 output.push_str(
3199 " fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n where\n S: serde::Serializer,\n {\n",
3200 );
3201 output.push_str(" use serde::ser::SerializeMap;\n");
3202 output.push_str(" match self {\n");
3203 for variant in variants {
3204 if variant.structured {
3205 if variant.payload.is_some() {
3206 if variant.payload_required {
3207 writeln!(
3208 output,
3209 " 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 }},",
3210 variant.name,
3211 quote_string(&variant.code)
3212 )
3213 .expect("String cannot fail");
3214 } else {
3215 writeln!(
3216 output,
3217 " 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 }},",
3218 variant.name,
3219 quote_string(&variant.code)
3220 )
3221 .expect("String cannot fail");
3222 }
3223 } else {
3224 writeln!(
3225 output,
3226 " Self::{} => {{\n let mut map = serializer.serialize_map(Some(1))?;\n map.serialize_entry(\"code\", {})?;\n map.end()\n }},",
3227 variant.name,
3228 quote_string(&variant.code)
3229 )
3230 .expect("String cannot fail");
3231 }
3232 } else {
3233 writeln!(
3234 output,
3235 " Self::{} => serializer.serialize_str({}),",
3236 variant.name,
3237 quote_string(&variant.code)
3238 )
3239 .expect("String cannot fail");
3240 }
3241 }
3242 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");
3243 writeln!(
3244 output,
3245 "impl<'de> serde::Deserialize<'de> for {error_name} {{"
3246 )
3247 .expect("String cannot fail");
3248 output.push_str(
3249 " 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",
3250 );
3251 for variant in variants.iter().filter(|variant| !variant.structured) {
3252 writeln!(
3253 output,
3254 " {} => Ok(Self::{}),",
3255 quote_string(&variant.code),
3256 variant.name
3257 )
3258 .expect("String cannot fail");
3259 }
3260 output.push_str(
3261 " _ => 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",
3262 );
3263 if variants.iter().any(|variant| variant.structured) {
3264 output.push_str(" match code.as_str() {\n");
3265 for variant in variants.iter().filter(|variant| variant.structured) {
3266 if variant.payload.is_some() {
3267 if variant.payload_required {
3268 writeln!(
3269 output,
3270 " {} => {{\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 }},",
3271 quote_string(&variant.code),
3272 variant.name
3273 )
3274 .expect("String cannot fail");
3275 } else {
3276 writeln!(
3277 output,
3278 " {} => {{\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 }},",
3279 quote_string(&variant.code),
3280 variant.name
3281 )
3282 .expect("String cannot fail");
3283 }
3284 } else {
3285 writeln!(
3286 output,
3287 " {} => Ok(Self::{}),",
3288 quote_string(&variant.code),
3289 variant.name
3290 )
3291 .expect("String cannot fail");
3292 }
3293 }
3294 output.push_str(
3295 " _ => {\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",
3296 );
3297 } else {
3298 output.push_str(
3299 " 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",
3300 );
3301 }
3302 output.push_str(
3303 " other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n }\n }\n}\n",
3304 );
3305 output
3306}
3307
3308fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
3309 let capability_name = pascal_case(
3310 contract
3311 .capability_id
3312 .split('@')
3313 .next()
3314 .and_then(|identity| identity.rsplit('.').next())
3315 .unwrap_or("Capability"),
3316 );
3317 let codec_name = format!("{capability_name}JsonCodec");
3318 let mut types = RustTypes::new();
3319 let mut operations = Vec::new();
3320 let mut encode_arms = Vec::new();
3321 let mut response_arms = Vec::new();
3322 let mut error_arms = Vec::new();
3323
3324 for operation in &contract.operations {
3325 if operation.interaction != "request" {
3326 return Err(CodegenError::UnsupportedInteraction {
3327 operation: operation.name.clone(),
3328 interaction: operation.interaction.clone(),
3329 });
3330 }
3331 let operation_name = pascal_case(&operation.name);
3332 let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
3333 let response_type =
3334 types.type_for(&operation.response, &format!("{operation_name}Response"));
3335 let error_type = format!("{operation_name}Error");
3336 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
3337 operations.push(operation_const.clone());
3338 encode_arms.push(format!(
3339 " {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 }}"
3340 ));
3341 response_arms.push(format!(
3342 " {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()),"
3343 ));
3344 error_arms.push(format!(
3345 " {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()),"
3346 ));
3347 }
3348
3349 let mut output = generate_rust(contract);
3350 write!(
3351 output,
3352 "\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 request_operations(&self) -> &'static [&'static str] {{\n &[{}]\n }}\n\n fn encode_request(\n &self,\n operation: &str,\n request: &dyn std::any::Any,\n ) -> Result<serde_json::Value, RuntimeFailure> {{\n match operation {{\n{},\n _ => Err(runtime_codec_unknown_operation(operation)),\n }}\n }}\n\n fn decode_response(\n &self,\n operation: &str,\n value: serde_json::Value,\n ) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n match operation {{\n{}\n _ => Err(runtime_codec_unknown_operation(operation)),\n }}\n }}\n\n fn decode_domain_error(\n &self,\n operation: &str,\n value: serde_json::Value,\n ) -> Result<Box<dyn std::any::Any>, RuntimeFailure> {{\n match operation {{\n{}\n _ => Err(runtime_codec_unknown_operation(operation)),\n }}\n }}\n}}\n\nfn runtime_codec_protocol_failure() -> RuntimeFailure {{\n RuntimeFailure::ProtocolViolation {{ capability: CAPABILITY_ID }}\n}}\n\nfn runtime_codec_unknown_operation(operation: &str) -> RuntimeFailure {{\n RuntimeFailure::UnknownOperation {{\n capability: CAPABILITY_ID,\n operation: operation.to_owned(),\n }}\n}}\n",
3353 operations.join(", "),
3354 encode_arms.join(",\n"),
3355 response_arms.join("\n"),
3356 error_arms.join("\n"),
3357 )
3358 .expect("writing generated Rust to a String cannot fail");
3359 Ok(output)
3360}
3361
3362fn generate_rust_wire_codecs(
3363 operation: &str,
3364 request_type: &str,
3365 response_type: &str,
3366 error_type: &str,
3367) -> String {
3368 let stem = snake_case(operation);
3369 format!(
3370 "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"
3371 )
3372}
3373
3374fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
3375 let stem = snake_case(operation);
3376 format!(
3377 "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"
3378 )
3379}
3380
3381fn generate_typescript_codecs(
3382 operation: &str,
3383 request_type: &str,
3384 response_type: &str,
3385 error_type: &str,
3386 variants: &[ErrorVariantIr],
3387) -> String {
3388 let stem = pascal_case(operation);
3389 let known_strings = variants
3390 .iter()
3391 .filter(|variant| !variant.structured)
3392 .map(|variant| quote_string(&variant.code))
3393 .collect::<Vec<_>>()
3394 .join(", ");
3395 format!(
3396 "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"
3397 )
3398}
3399
3400fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
3401 let stem = pascal_case(operation);
3402 format!(
3403 "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"
3404 )
3405}
3406
3407#[allow(clippy::too_many_lines)]
3408fn generate_typescript(contract: &ContractIr) -> String {
3409 let capability_name = pascal_case(
3410 contract
3411 .capability_id
3412 .split('@')
3413 .next()
3414 .and_then(|identity| identity.rsplit('.').next())
3415 .unwrap_or("Capability"),
3416 );
3417 let mut types = TypeScriptTypes::new();
3418 let mut clients = Vec::new();
3419 let mut providers = Vec::new();
3420 let mut errors = Vec::new();
3421 let mut codecs = Vec::new();
3422 let mut request_dispatch_arms = Vec::new();
3423 let operation_names = contract
3424 .operations
3425 .iter()
3426 .map(|operation| quote_string(&operation.name))
3427 .collect::<Vec<_>>()
3428 .join(", ");
3429 let stream_operation_names = contract
3430 .operations
3431 .iter()
3432 .filter(|operation| operation.interaction == "stream")
3433 .map(|operation| quote_string(&operation.name))
3434 .collect::<Vec<_>>()
3435 .join(", ");
3436 let event_operation_names = contract
3437 .operations
3438 .iter()
3439 .filter(|operation| operation.interaction == "event")
3440 .map(|operation| quote_string(&operation.name))
3441 .collect::<Vec<_>>()
3442 .join(", ");
3443 let has_event_operations = contract
3444 .operations
3445 .iter()
3446 .any(|operation| operation.interaction == "event");
3447 for operation in &contract.operations {
3448 let operation_name = pascal_case(&operation.name);
3449 let request_name = format!("{operation_name}Request");
3450 let response_name = format!("{operation_name}Response");
3451 let error_name = format!("{operation_name}Error");
3452 let request_type = types.type_for(&operation.request, &request_name);
3453 let response_type = types.type_for(&operation.response, &response_name);
3454 let variants = &operation.domain_errors;
3455 let error_type = if variants.is_empty() {
3456 "UnknownDomainError".to_owned()
3457 } else {
3458 let mut values = variants
3459 .iter()
3460 .map(|variant| {
3461 if let Some(payload) = &variant.payload {
3462 let payload_name = format!("{error_name}{}Payload", variant.name);
3463 let payload_type =
3464 types.type_for_non_null(payload.non_null(), &payload_name);
3465 let payload_type = if payload.is_nullable() {
3466 format!("{payload_type} | null")
3467 } else {
3468 payload_type
3469 };
3470 let payload_optional = if variant.payload_required { "" } else { "?" };
3471 format!(
3472 "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
3473 quote_string(&variant.code),
3474 )
3475 } else if variant.structured {
3476 format!("{{ readonly code: {} }}", quote_string(&variant.code))
3477 } else {
3478 quote_string(&variant.code)
3479 }
3480 })
3481 .collect::<Vec<_>>();
3482 values.push("UnknownDomainError".to_owned());
3483 values.join(" | ")
3484 };
3485 let invocation_error_name = format!("{operation_name}InvocationError");
3486 let result_name = format!("{operation_name}Result");
3487 let result_value_type = match operation.interaction.as_str() {
3488 "stream" => format!("StreamSession<{response_type}, {error_name}>"),
3489 "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
3490 _ => response_type.clone(),
3491 };
3492 if operation.interaction == "event" {
3493 errors.push(format!(
3494 "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
3495 ));
3496 } else {
3497 errors.push(format!(
3498 "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} }};"
3499 ));
3500 }
3501 codecs.push(generate_typescript_codecs(
3502 &operation.name,
3503 &request_type,
3504 &response_type,
3505 &error_name,
3506 variants,
3507 ));
3508 if operation.interaction == "event" {
3509 codecs.push(generate_typescript_event_codecs(
3510 &operation.name,
3511 &request_type,
3512 ));
3513 }
3514 if matches!(operation.interaction.as_str(), "request" | "stream") {
3515 clients.push(format!(
3516 " {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
3517 typescript_property_name(&snake_case(&operation.name)),
3518 ));
3519 providers.push(format!(
3520 " {}(context: InvocationContext, request: {request_type}): Promise<{result_name}>;",
3521 typescript_property_name(&snake_case(&operation.name)),
3522 ));
3523 } else {
3524 clients.push(format!(
3525 " {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
3526 typescript_property_name(&snake_case(&operation.name)),
3527 ));
3528 providers.push(format!(
3529 " {}(context: InvocationContext, event: {request_type}): void;",
3530 typescript_property_name(&snake_case(&operation.name)),
3531 ));
3532 }
3533 if operation.interaction == "request" {
3534 let provider_method = typescript_property_name(&snake_case(&operation.name));
3535 request_dispatch_arms.push(format!(
3536 " 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: \"module_failure\", detail: providerErrorMessage(error) }} }};\n }}\n }}",
3537 quote_string(&operation.name),
3538 ));
3539 }
3540 }
3541 let mut output = String::new();
3542 output.push_str(TYPESCRIPT_HEADER);
3543 output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
3544 writeln!(
3545 output,
3546 "export const CAPABILITY_ID = {};",
3547 quote_string(&contract.capability_id)
3548 )
3549 .expect("writing to a String cannot fail");
3550 writeln!(
3551 output,
3552 "export const DESCRIPTOR_VERSION = {};",
3553 quote_string(&contract.version)
3554 )
3555 .expect("writing to a String cannot fail");
3556 writeln!(output, "export const PORTABLE = {};", contract.portable)
3557 .expect("writing to a String cannot fail");
3558 write!(
3559 output,
3560 "export const CROSS_LANE_TRANSFER = {};\n\n",
3561 contract.cross_lane_transfer
3562 )
3563 .expect("writing to a String cannot fail");
3564 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");
3565 if has_event_operations {
3566 output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
3567 }
3568 for declaration in types.declarations {
3569 output.push_str(&declaration);
3570 output.push('\n');
3571 }
3572 for error in errors {
3573 output.push_str(&error);
3574 output.push('\n');
3575 }
3576 for codec in codecs {
3577 output.push_str(&codec);
3578 output.push('\n');
3579 }
3580 write!(
3581 output,
3582 "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
3583 clients.join("\n"),
3584 providers.join("\n")
3585 )
3586 .expect("writing to a String cannot fail");
3587 write!(
3588 output,
3589 "\nexport type ProviderDispatchOutcome =\n | {{ readonly kind: \"success\"; readonly value: unknown }}\n | {{ readonly kind: \"domain\"; readonly value: unknown }}\n | {{ readonly kind: \"runtime\"; readonly failure: RuntimeFailure }};\n\nexport interface CapabilityProviderDescriptor {{\n readonly capability_id: string;\n readonly descriptor_version: string;\n readonly operations: ReadonlyArray<string>;\n readonly stream_operations: ReadonlyArray<string>;\n readonly event_operations: ReadonlyArray<string>;\n}}\n\nexport interface CapabilityProviderBinding {{\n readonly descriptor: CapabilityProviderDescriptor;\n invokeRequest(\n operation: string,\n context: InvocationContext,\n payload: unknown,\n ): Promise<ProviderDispatchOutcome>;\n}}\n\nfunction providerErrorMessage(error: unknown): string {{\n return error instanceof Error ? error.message : String(error);\n}}\n\nexport function bind{capability_name}Provider(\n provider: {capability_name}Provider,\n): CapabilityProviderBinding {{\n return {{\n descriptor: {{\n capability_id: CAPABILITY_ID,\n descriptor_version: DESCRIPTOR_VERSION,\n operations: [{operation_names}],\n stream_operations: [{stream_operation_names}],\n event_operations: [{event_operation_names}],\n }},\n async invokeRequest(operation, context, payload) {{\n switch (operation) {{\n{}\n default:\n return {{ kind: \"runtime\", failure: {{ kind: \"unknown_operation\", operation }} }};\n }}\n }},\n }};\n}}\n\nexport type Provider = {capability_name}Provider;\nexport const bindProvider = bind{capability_name}Provider;\n",
3590 request_dispatch_arms.join("\n")
3591 )
3592 .expect("writing to a String cannot fail");
3593 output.push_str(
3594 "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
3595 );
3596 output
3597}