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