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