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