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