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