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