1use std::{
10 collections::{BTreeMap, BTreeSet, HashSet},
11 error::Error,
12 fmt,
13 fmt::Write as _,
14 fs,
15 path::{Path, PathBuf},
16};
17
18use regex::Regex;
19use serde_json::{Map, Value};
20
21mod browser;
22mod ir;
23mod rust_debug;
24mod source;
25mod wit;
26
27pub use browser::generate_browser_request_client;
28pub use source::{check_source_snapshot, write_source_snapshot};
29
30use ir::{ContractIr, ErrorVariantIr, FieldIr, ObjectAdditionalIr, OperationIr, TypeIr};
31
32const GENERATED_HEADER: &str = "// @generated by lenso-contract-codegen; do not edit.\n";
33const TYPESCRIPT_HEADER: &str = "/* @generated by lenso-contract-codegen; do not edit. */\n";
34const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
35
36#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
38struct Version(semver::Version);
39
40impl Version {
41 fn parse(value: &str) -> Result<Self, CodegenError> {
42 semver::Version::parse(value)
43 .map(Self)
44 .map_err(|error| CodegenError::InvalidDescriptor {
45 detail: format!("Descriptor version `{value}` is not valid SemVer: {error}"),
46 })
47 }
48
49 const fn major(&self) -> u64 {
50 self.0.major
51 }
52
53 const fn minor(&self) -> u64 {
54 self.0.minor
55 }
56}
57
58fn parse_numeric_component(value: &str) -> Option<u64> {
59 (!value.is_empty() && (value == "0" || !value.starts_with('0')))
60 .then(|| value.parse().ok())
61 .flatten()
62}
63
64#[derive(Clone, Debug)]
65struct Operation {
66 name: String,
67 interaction: String,
68 request_schema: Value,
69 response_schema: Value,
70 domain_error_schema: Value,
71}
72
73fn contract_ir(descriptor: &Descriptor) -> ContractIr {
74 ContractIr {
75 capability_id: descriptor.capability_id.clone(),
76 version: descriptor.version.clone(),
77 portable: descriptor.portable,
78 cross_lane_transfer: descriptor.cross_lane_transfer,
79 operations: descriptor
80 .operations
81 .iter()
82 .map(|operation| OperationIr {
83 name: operation.name.clone(),
84 interaction: operation.interaction.clone(),
85 request: type_ir_from_schema(&operation.request_schema),
86 response: type_ir_from_schema(&operation.response_schema),
87 domain_errors: error_variant_ir_definitions(&operation.domain_error_schema),
88 })
89 .collect(),
90 }
91}
92
93fn type_ir_from_schema(schema: &Value) -> TypeIr {
94 if let Some(types) = schema.get("type").and_then(Value::as_array) {
95 let non_null_types = types
96 .iter()
97 .filter(|schema_type| *schema_type != "null")
98 .cloned()
99 .collect::<Vec<_>>();
100 if non_null_types.is_empty() {
101 return TypeIr::Null;
102 }
103 if non_null_types.len() == 1 {
104 let mut narrowed = schema.as_object().cloned().unwrap_or_default();
105 narrowed.insert("type".to_owned(), non_null_types[0].clone());
106 let base = type_ir_non_null(&Value::Object(narrowed));
107 return if non_null_types.len() == types.len() {
108 base
109 } else {
110 TypeIr::Nullable(Box::new(base))
111 };
112 }
113 }
114 type_ir_non_null(schema)
115}
116
117fn type_ir_non_null(schema: &Value) -> TypeIr {
118 let Some(object) = schema.as_object() else {
119 return TypeIr::Any;
120 };
121 if let Some(values) = object.get("enum").and_then(Value::as_array) {
122 return TypeIr::Enum {
123 name: schema_declared_type_name(object),
124 values: values
125 .iter()
126 .filter_map(Value::as_str)
127 .map(ToOwned::to_owned)
128 .collect(),
129 };
130 }
131 if let Some(schema_type) = object.get("type").and_then(Value::as_str) {
132 return match schema_type {
133 "object" => {
134 let required = required_fields(schema);
135 let mut properties = object
136 .get("properties")
137 .and_then(Value::as_object)
138 .into_iter()
139 .flatten()
140 .collect::<Vec<_>>();
141 properties.sort_unstable_by_key(|(name, _)| *name);
146 let fields = properties
147 .into_iter()
148 .map(|(name, schema)| {
149 let ty = type_ir_from_schema(schema);
150 let ty = if name.ends_with("_json") && ty == TypeIr::String {
151 TypeIr::RawJson
152 } else {
153 ty
154 };
155 FieldIr {
156 name: name.clone(),
157 required: required.contains(name),
158 sensitive: schema
159 .get("x-lenso-sensitive")
160 .and_then(Value::as_bool)
161 .unwrap_or(false),
162 ty,
163 }
164 })
165 .collect();
166 let additional = match object.get("additionalProperties") {
167 Some(Value::Bool(false)) => ObjectAdditionalIr::Closed,
168 Some(Value::Bool(true)) | None => ObjectAdditionalIr::Any,
169 Some(schema) => {
170 ObjectAdditionalIr::Typed(Box::new(type_ir_from_schema(schema)))
171 }
172 };
173 TypeIr::Object {
174 name: schema_declared_type_name(object),
175 fields,
176 additional,
177 }
178 }
179 "array" => object.get("items").map_or(TypeIr::Any, |items| {
180 TypeIr::Array(Box::new(type_ir_from_schema(items)))
181 }),
182 "string" => match object.get("format").and_then(Value::as_str) {
183 Some("int64") => TypeIr::Int64,
184 Some("uint64") => TypeIr::Uint64,
185 Some("byte") => TypeIr::Bytes,
186 Some("date-time") => TypeIr::Timestamp,
187 Some("duration") => TypeIr::Duration,
188 _ => TypeIr::String,
189 },
190 "integer" => TypeIr::Integer,
191 "number" => TypeIr::Number,
192 "boolean" => TypeIr::Boolean,
193 "null" => TypeIr::Null,
194 _ => TypeIr::Any,
195 };
196 }
197 TypeIr::Any
198}
199
200fn schema_declared_type_name(schema: &Map<String, Value>) -> Option<String> {
201 schema
202 .get("title")
203 .and_then(Value::as_str)
204 .filter(|title| !title.is_empty())
205 .map(pascal_case)
206}
207
208#[derive(Clone, Debug)]
210pub struct Descriptor {
211 capability_id: String,
212 capability_major: u64,
213 version: String,
214 parsed_version: Version,
215 portable: bool,
216 cross_lane_transfer: bool,
217 operations: Vec<Operation>,
218}
219
220impl Descriptor {
221 #[must_use]
223 pub fn capability_id(&self) -> &str {
224 &self.capability_id
225 }
226
227 #[must_use]
229 pub const fn capability_major(&self) -> u64 {
230 self.capability_major
231 }
232
233 #[must_use]
235 pub fn version(&self) -> &str {
236 &self.version
237 }
238
239 #[must_use]
241 pub const fn portable(&self) -> bool {
242 self.portable
243 }
244
245 #[must_use]
247 pub const fn cross_lane_transfer(&self) -> bool {
248 self.cross_lane_transfer
249 }
250
251 #[must_use]
253 pub fn operation_names(&self) -> Vec<&str> {
254 self.operations
255 .iter()
256 .map(|operation| operation.name.as_str())
257 .collect()
258 }
259}
260
261#[derive(Clone, Debug, Eq, PartialEq)]
263pub struct GeneratedMetadata {
264 pub capability_id: String,
266 pub descriptor_version: String,
268 pub portable: bool,
270 pub cross_lane_transfer: bool,
272}
273
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
276pub enum ProjectionLanguage {
277 Rust,
279 TypeScript,
281 Wit,
283 RustRuntime,
285}
286
287#[derive(Clone, Debug, Eq, PartialEq)]
289pub struct GeneratedProjection {
290 pub metadata: GeneratedMetadata,
292 pub language: ProjectionLanguage,
294 pub source: String,
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct GeneratedArtifacts {
301 pub metadata: GeneratedMetadata,
303 pub rust: String,
305 pub typescript: String,
307}
308
309#[derive(Debug)]
311pub enum CodegenError {
312 Io {
314 path: PathBuf,
315 source: std::io::Error,
316 },
317 InvalidDescriptor { detail: String },
319 UnsupportedSchema { path: PathBuf, detail: String },
321 UnsupportedInteraction {
323 operation: String,
324 interaction: String,
325 },
326 UnsupportedWit { detail: String },
328 InvalidPortableValue { path: String, detail: String },
330 GeneratedArtifactDrift { path: PathBuf },
332}
333
334impl fmt::Display for CodegenError {
335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336 match self {
337 Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
338 Self::InvalidDescriptor { detail } => {
339 write!(formatter, "invalid Descriptor: {detail}")
340 }
341 Self::UnsupportedSchema { path, detail } => {
342 write!(formatter, "unsupported Schema {}: {detail}", path.display())
343 }
344 Self::UnsupportedInteraction {
345 operation,
346 interaction,
347 } => write!(
348 formatter,
349 "Operation `{operation}` uses unsupported interaction `{interaction}`"
350 ),
351 Self::UnsupportedWit { detail } => {
352 write!(formatter, "unsupported WIT projection: {detail}")
353 }
354 Self::InvalidPortableValue { path, detail } => {
355 write!(formatter, "invalid portable value at `{path}`: {detail}")
356 }
357 Self::GeneratedArtifactDrift { path } => {
358 write!(formatter, "generated artifact is stale: {}", path.display())
359 }
360 }
361 }
362}
363
364impl Error for CodegenError {
365 fn source(&self) -> Option<&(dyn Error + 'static)> {
366 match self {
367 Self::Io { source, .. } => Some(source),
368 _ => None,
369 }
370 }
371}
372
373#[derive(Clone, Debug, Eq, PartialEq)]
376pub enum CompatibilityError {
377 IdentityChanged { from: String, to: String },
379 VersionNotAdvanced { from: String, to: String },
381 BreakingChanges { changes: Vec<String> },
383}
384
385impl fmt::Display for CompatibilityError {
386 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
387 match self {
388 Self::IdentityChanged { from, to } => {
389 write!(
390 formatter,
391 "Capability identity changed from `{from}` to `{to}`"
392 )
393 }
394 Self::VersionNotAdvanced { from, to } => {
395 write!(
396 formatter,
397 "Descriptor version must advance from `{from}` to `{to}`"
398 )
399 }
400 Self::BreakingChanges { changes } => write!(
401 formatter,
402 "breaking Descriptor changes require a new major: {}",
403 changes.join("; ")
404 ),
405 }
406 }
407}
408
409impl Error for CompatibilityError {}
410
411#[allow(clippy::too_many_lines)]
413pub fn load_descriptor(path: &Path) -> Result<Descriptor, CodegenError> {
414 let descriptor_path = canonical_path(path)?;
415 let package_root = descriptor_path
416 .parent()
417 .unwrap_or_else(|| Path::new("."))
418 .to_path_buf();
419 let descriptor_value = read_json(&descriptor_path)?;
420 let object = descriptor_value
421 .as_object()
422 .ok_or_else(|| CodegenError::InvalidDescriptor {
423 detail: "the Descriptor root must be an object".to_owned(),
424 })?;
425
426 let capability_id = required_string(object, "id")?;
427 let (identity, identity_major) = capability_identity(&capability_id)?;
428 let version = required_string(object, "version")?;
429 let parsed_version = Version::parse(&version)?;
430 let portable = object
431 .get("portable")
432 .and_then(Value::as_bool)
433 .unwrap_or(false);
434 let cross_lane_transfer = object
435 .get("cross_lane_transfer")
436 .and_then(Value::as_bool)
437 .unwrap_or(false);
438 let operation_values = object
439 .get("operations")
440 .and_then(Value::as_array)
441 .ok_or_else(|| CodegenError::InvalidDescriptor {
442 detail: "`operations` must be an array".to_owned(),
443 })?;
444 if operation_values.is_empty() {
445 return Err(CodegenError::InvalidDescriptor {
446 detail: "a Descriptor must declare at least one Operation".to_owned(),
447 });
448 }
449
450 let mut operations = Vec::with_capacity(operation_values.len());
451 let mut operation_names = BTreeSet::new();
452 let mut generated_operation_names = BTreeSet::new();
453 let mut generated_type_names = BTreeSet::new();
454 let mut generated_client_method_names = BTreeSet::from([
455 "new".to_owned(),
456 "from_context".to_owned(),
457 "from_dependencies".to_owned(),
458 ]);
459 let capability_name = identity
460 .rsplit('.')
461 .next()
462 .map_or_else(|| "Capability".to_owned(), pascal_case);
463 for operation_value in operation_values {
464 let operation =
465 operation_value
466 .as_object()
467 .ok_or_else(|| CodegenError::InvalidDescriptor {
468 detail: "each Operation must be an object".to_owned(),
469 })?;
470 let name = required_string(operation, "name")?;
471 if !is_identifier(&name) || !is_rust_member_name(&name) {
472 return Err(CodegenError::InvalidDescriptor {
473 detail: format!(
474 "Operation name `{name}` must start with a letter or `_` and contain only letters, digits, `_`, or `-`"
475 ),
476 });
477 }
478 if !operation_names.insert(name.clone()) {
479 return Err(CodegenError::InvalidDescriptor {
480 detail: format!("Operation `{name}` is declared more than once"),
481 });
482 }
483 for generated_name in [rust_field_name(&name), pascal_case(&name)] {
484 if !generated_operation_names.insert(generated_name.clone()) {
485 return Err(CodegenError::InvalidDescriptor {
486 detail: format!(
487 "Operation name `{name}` collides after code generation as `{generated_name}`"
488 ),
489 });
490 }
491 }
492 let operation_name = pascal_case(&name);
493 let mut generated_names = vec![
494 format!("{operation_name}Request"),
495 format!("{operation_name}Response"),
496 format!("{operation_name}Error"),
497 format!("{operation_name}InvocationError"),
498 format!("{operation_name}Result"),
499 format!("{capability_name}{operation_name}"),
500 ];
501 generated_names.push(if operation_values.len() == 1 {
502 format!("{capability_name}InvocationError")
503 } else {
504 format!("{capability_name}{operation_name}InvocationError")
505 });
506 for generated_name in generated_names {
507 if !is_generated_type_name(&generated_name)
508 || !generated_type_names.insert(generated_name.clone())
509 {
510 return Err(CodegenError::InvalidDescriptor {
511 detail: format!(
512 "Operation name `{name}` collides after type generation as `{generated_name}`"
513 ),
514 });
515 }
516 }
517 let interaction = required_string(operation, "interaction")?;
518 if !matches!(interaction.as_str(), "request" | "stream" | "event") {
519 return Err(CodegenError::InvalidDescriptor {
520 detail: format!("Operation `{name}` has unsupported interaction `{interaction}`"),
521 });
522 }
523 if matches!(interaction.as_str(), "request" | "stream" | "event") {
524 let client_method_name = rust_field_name(&name);
525 for generated_name in [
526 client_method_name.clone(),
527 format!("{client_method_name}_with_context"),
528 ] {
529 if !generated_client_method_names.insert(generated_name.clone()) {
530 return Err(CodegenError::InvalidDescriptor {
531 detail: format!(
532 "Operation name `{name}` collides with the generated Client API as `{generated_name}`"
533 ),
534 });
535 }
536 }
537 }
538 let request_schema_path =
539 schema_path(&descriptor_path, operation, "request_schema", &name)?;
540 let response_schema_path =
541 schema_path(&descriptor_path, operation, "response_schema", &name)?;
542 let domain_error_schema_path =
543 schema_path(&descriptor_path, operation, "domain_error_schema", &name)?;
544 let mut ref_stack = Vec::new();
545 let request_source = read_json(&request_schema_path)?;
546 let mut request_schema = resolve_refs(
547 &request_source,
548 &request_source,
549 &request_schema_path,
550 &package_root,
551 &mut ref_stack,
552 )?;
553 normalize_nullable_unions(&mut request_schema);
554 validate_schema_profile(&request_schema, &request_schema_path)?;
555 validate_value_generation_schema(&request_schema, &request_schema_path)?;
556 let mut ref_stack = Vec::new();
557 let response_source = read_json(&response_schema_path)?;
558 let mut response_schema = resolve_refs(
559 &response_source,
560 &response_source,
561 &response_schema_path,
562 &package_root,
563 &mut ref_stack,
564 )?;
565 normalize_nullable_unions(&mut response_schema);
566 validate_schema_profile(&response_schema, &response_schema_path)?;
567 validate_value_generation_schema(&response_schema, &response_schema_path)?;
568 let mut ref_stack = Vec::new();
569 let domain_error_source = read_json(&domain_error_schema_path)?;
570 let mut domain_error_schema = resolve_refs(
571 &domain_error_source,
572 &domain_error_source,
573 &domain_error_schema_path,
574 &package_root,
575 &mut ref_stack,
576 )?;
577 normalize_nullable_unions(&mut domain_error_schema);
578 validate_schema_profile(&domain_error_schema, &domain_error_schema_path)?;
579 validate_domain_error_schema(&domain_error_schema, &domain_error_schema_path)?;
580 operations.push(Operation {
581 name,
582 interaction,
583 request_schema,
584 response_schema,
585 domain_error_schema,
586 });
587 }
588 operations.sort_by(|left, right| left.name.cmp(&right.name));
589
590 Ok(Descriptor {
591 capability_id: format!("{identity}@{identity_major}"),
592 capability_major: identity_major,
593 version,
594 parsed_version,
595 portable,
596 cross_lane_transfer,
597 operations,
598 })
599}
600
601fn required_string(object: &Map<String, Value>, key: &str) -> Result<String, CodegenError> {
602 object
603 .get(key)
604 .and_then(Value::as_str)
605 .filter(|value| !value.is_empty())
606 .map(ToOwned::to_owned)
607 .ok_or_else(|| CodegenError::InvalidDescriptor {
608 detail: format!("`{key}` must be a non-empty string"),
609 })
610}
611
612fn capability_identity(value: &str) -> Result<(String, u64), CodegenError> {
613 let Some((identity, major)) = value.rsplit_once('@') else {
614 return Err(CodegenError::InvalidDescriptor {
615 detail: format!("Capability id `{value}` must use `namespace.name@major`"),
616 });
617 };
618 let generated_name = identity
619 .rsplit('.')
620 .next()
621 .map(pascal_case)
622 .unwrap_or_default();
623 if identity.is_empty()
624 || identity.split('.').count() < 2
625 || !identity.split('.').all(is_identifier)
626 || !is_generated_type_name(&generated_name)
627 {
628 return Err(CodegenError::InvalidDescriptor {
629 detail: format!("Capability id `{value}` must use `namespace.name@major`"),
630 });
631 }
632 let Some(major) = parse_numeric_component(major) else {
633 return Err(CodegenError::InvalidDescriptor {
634 detail: format!("Capability id `{value}` has an invalid major"),
635 });
636 };
637 Ok((identity.to_owned(), major))
638}
639
640fn schema_path(
641 descriptor_path: &Path,
642 operation: &Map<String, Value>,
643 key: &str,
644 operation_name: &str,
645) -> Result<PathBuf, CodegenError> {
646 let value = required_string(operation, key).map_err(|error| match error {
647 CodegenError::InvalidDescriptor { detail } => CodegenError::InvalidDescriptor {
648 detail: format!("Operation `{operation_name}`: {detail}"),
649 },
650 other => other,
651 })?;
652 let candidate = descriptor_path
653 .parent()
654 .unwrap_or_else(|| Path::new("."))
655 .join(value);
656 let canonical = canonical_path(&candidate)?;
657 let package_root = canonical_path(descriptor_path.parent().unwrap_or_else(|| Path::new(".")))?;
658 if !canonical.starts_with(&package_root) {
659 return Err(CodegenError::UnsupportedSchema {
660 path: canonical,
661 detail: "Schema paths must remain inside the Descriptor package".to_owned(),
662 });
663 }
664 Ok(canonical)
665}
666
667fn canonical_path(path: &Path) -> Result<PathBuf, CodegenError> {
668 fs::canonicalize(path).map_err(|source| CodegenError::Io {
669 path: path.to_path_buf(),
670 source,
671 })
672}
673
674fn read_json(path: &Path) -> Result<Value, CodegenError> {
675 let source = fs::read_to_string(path).map_err(|source| CodegenError::Io {
676 path: path.to_path_buf(),
677 source,
678 })?;
679 serde_json::from_str(&source).map_err(|error| CodegenError::InvalidDescriptor {
680 detail: format!("{}: {error}", path.display()),
681 })
682}
683
684fn resolve_refs(
685 value: &Value,
686 root: &Value,
687 source_path: &Path,
688 package_root: &Path,
689 stack: &mut Vec<(PathBuf, String)>,
690) -> Result<Value, CodegenError> {
691 match value {
692 Value::Object(object) => {
693 if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
694 let target_value = if reference.starts_with('#') {
695 let fragment = reference.strip_prefix('#').unwrap_or_default();
696 let target_path = source_path.to_path_buf();
697 let key = (target_path.clone(), reference.to_owned());
698 if stack.contains(&key) {
699 return Err(CodegenError::UnsupportedSchema {
700 path: source_path.to_path_buf(),
701 detail: format!("cyclic local Schema `$ref` `{reference}`"),
702 });
703 }
704 let target = json_pointer(root, fragment).ok_or_else(|| {
705 CodegenError::UnsupportedSchema {
706 path: source_path.to_path_buf(),
707 detail: format!(
708 "local JSON Pointer `$ref` `{reference}` was not found"
709 ),
710 }
711 })?;
712 stack.push(key);
713 let resolved = resolve_refs(target, root, source_path, package_root, stack)?;
714 stack.pop();
715 resolved
716 } else {
717 let (reference_path, fragment) =
718 reference.split_once('#').unwrap_or((reference, ""));
719 let target_path = source_path
720 .parent()
721 .unwrap_or_else(|| Path::new("."))
722 .join(reference_path);
723 let target_path = canonical_path(&target_path)?;
724 if !target_path.starts_with(package_root) {
725 return Err(CodegenError::UnsupportedSchema {
726 path: source_path.to_path_buf(),
727 detail: format!(
728 "external Schema `$ref` `{reference}` leaves the Descriptor package"
729 ),
730 });
731 }
732 let key = (target_path.clone(), format!("#{fragment}"));
733 if stack.contains(&key) {
734 return Err(CodegenError::UnsupportedSchema {
735 path: source_path.to_path_buf(),
736 detail: format!(
737 "cyclic Schema `$ref` through {}",
738 target_path.display()
739 ),
740 });
741 }
742 let target_root = read_json(&target_path)?;
743 let target = json_pointer(&target_root, fragment).ok_or_else(|| {
744 CodegenError::UnsupportedSchema {
745 path: source_path.to_path_buf(),
746 detail: format!(
747 "external JSON Pointer `$ref` `{reference}` was not found"
748 ),
749 }
750 })?;
751 stack.push(key);
752 let resolved =
753 resolve_refs(target, &target_root, &target_path, package_root, stack)?;
754 stack.pop();
755 resolved
756 };
757 let mut merged = target_value.as_object().cloned().ok_or_else(|| {
758 CodegenError::UnsupportedSchema {
759 path: source_path.to_path_buf(),
760 detail: "a `$ref` target must resolve to an object Schema".to_owned(),
761 }
762 })?;
763 restore_reference_type_name(&mut merged, reference);
764 for (key, child) in object {
765 if key != "$ref" {
766 merged.insert(
767 key.clone(),
768 resolve_refs(child, root, source_path, package_root, stack)?,
769 );
770 }
771 }
772 return Ok(Value::Object(merged));
773 }
774 let mut resolved = Map::new();
775 for (key, child) in object {
776 resolved.insert(
777 key.clone(),
778 resolve_refs(child, root, source_path, package_root, stack)?,
779 );
780 }
781 Ok(Value::Object(resolved))
782 }
783 Value::Array(values) => values
784 .iter()
785 .map(|child| resolve_refs(child, root, source_path, package_root, stack))
786 .collect::<Result<Vec<_>, _>>()
787 .map(Value::Array),
788 _ => Ok(value.clone()),
789 }
790}
791
792fn reference_definition_name(reference: &str) -> Option<String> {
793 let (_, fragment) = reference.rsplit_once('#')?;
794 let name = fragment.strip_prefix("/$defs/")?;
795 if name.is_empty() || name.contains('/') {
796 return None;
797 }
798 Some(name.replace("~1", "/").replace("~0", "~"))
799}
800
801fn restore_reference_type_name(schema: &mut Map<String, Value>, reference: &str) {
802 if schema.get("title").is_none()
803 && let Some(name) = reference_definition_name(reference)
804 {
805 schema.insert("title".to_owned(), Value::String(name));
806 }
807}
808
809fn json_pointer<'a>(root: &'a Value, fragment: &str) -> Option<&'a Value> {
810 if fragment.is_empty() {
811 return Some(root);
812 }
813 let pointer = fragment.strip_prefix('/')?;
814 let mut current = root;
815 for token in pointer.split('/') {
816 let token = token.replace("~1", "/").replace("~0", "~");
817 current = match current {
818 Value::Object(object) => object.get(&token)?,
819 Value::Array(array) => array.get(token.parse::<usize>().ok()?)?,
820 _ => return None,
821 };
822 }
823 Some(current)
824}
825
826fn normalize_nullable_unions(value: &mut Value) {
827 match value {
828 Value::Object(object) => {
829 for child in object.values_mut() {
830 normalize_nullable_unions(child);
831 }
832 let nullable = ["anyOf", "oneOf"].into_iter().find_map(|key| {
833 let alternatives = object.get(key)?.as_array()?;
834 if alternatives.len() != 2 {
835 return None;
836 }
837 let null_index = alternatives.iter().position(|alternative| {
838 alternative.get("type").and_then(Value::as_str) == Some("null")
839 })?;
840 let non_null = alternatives.get(1 - null_index)?.as_object()?.clone();
841 let schema_type = non_null
842 .get("type")
843 .and_then(Value::as_str)
844 .map(ToOwned::to_owned)?;
845 Some((key, schema_type, non_null))
846 });
847 if let Some((union_key, schema_type, mut normalized)) = nullable {
848 normalized.insert(
849 "type".to_owned(),
850 Value::Array(vec![
851 Value::String(schema_type),
852 Value::String("null".to_owned()),
853 ]),
854 );
855 for (key, child) in std::mem::take(object) {
856 if key != union_key {
857 normalized.entry(key).or_insert(child);
858 }
859 }
860 *object = normalized;
861 }
862 }
863 Value::Array(values) => {
864 for child in values {
865 normalize_nullable_unions(child);
866 }
867 }
868 _ => {}
869 }
870}
871
872const SUPPORTED_SCHEMA_KEYWORDS: &[&str] = &[
873 "$defs",
874 "additionalProperties",
875 "anyOf",
876 "const",
877 "else",
878 "enum",
879 "exclusiveMaximum",
880 "exclusiveMinimum",
881 "format",
882 "if",
883 "items",
884 "maxItems",
885 "maxLength",
886 "maxProperties",
887 "maximum",
888 "minItems",
889 "minLength",
890 "minProperties",
891 "minimum",
892 "oneOf",
893 "pattern",
894 "properties",
895 "propertyNames",
896 "required",
897 "then",
898 "type",
899 "uniqueItems",
900];
901
902const HARMLESS_SCHEMA_ANNOTATIONS: &[&str] = &[
903 "$anchor",
904 "$comment",
905 "$dynamicAnchor",
906 "$id",
907 "$schema",
908 "default",
909 "deprecated",
910 "description",
911 "examples",
912 "readOnly",
913 "title",
914 "writeOnly",
915 "x-lenso-max-container-items",
916 "x-lenso-max-depth",
917 "x-lenso-max-encoded-bytes",
918 "x-lenso-max-nodes",
919 "x-lenso-max-string-length",
920 "x-lenso-sensitive",
921];
922
923const PORTABLE_SCHEMA_TYPES: &[&str] = &[
924 "array", "boolean", "integer", "null", "number", "object", "string",
925];
926
927const PORTABLE_STRING_FORMATS: &[&str] = &["byte", "date-time", "duration", "int64", "uint64"];
928const MAX_PORTABLE_PATTERN_CHARS: usize = 4_096;
929const MAX_PORTABLE_REPETITION: u32 = 10_000;
930const PORTABLE_UNICODE_PROPERTY_ESCAPE: &str = r"\p{Letter}";
931
932fn validate_schema_profile(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
933 let Some(object) = schema.as_object() else {
934 return Err(unsupported_schema(
935 source_path,
936 "a JSON Schema must be an object",
937 ));
938 };
939
940 for keyword in object.keys() {
941 if !SUPPORTED_SCHEMA_KEYWORDS.contains(&keyword.as_str())
942 && !HARMLESS_SCHEMA_ANNOTATIONS.contains(&keyword.as_str())
943 {
944 return Err(unsupported_schema(
945 source_path,
946 format!("Schema keyword `{keyword}` is outside the portable profile"),
947 ));
948 }
949 }
950
951 validate_schema_type_keyword(object, source_path)?;
952 validate_schema_composition_keywords(object, source_path)?;
953 validate_schema_scalar_keywords(object, source_path)?;
954 validate_schema_numeric_keywords(object, source_path)?;
955 validate_schema_children(object, source_path)
956}
957
958fn validate_schema_type_keyword(
959 object: &Map<String, Value>,
960 source_path: &Path,
961) -> Result<(), CodegenError> {
962 if let Some(schema_type) = object.get("type") {
963 match schema_type {
964 Value::String(schema_type) => validate_schema_type(schema_type, source_path)?,
965 Value::Array(types) if !types.is_empty() => {
966 let mut unique = BTreeSet::new();
967 for schema_type in types {
968 let Some(schema_type) = schema_type.as_str() else {
969 return Err(unsupported_schema(
970 source_path,
971 "Schema type unions must contain strings",
972 ));
973 };
974 validate_schema_type(schema_type, source_path)?;
975 if !unique.insert(schema_type) {
976 return Err(unsupported_schema(
977 source_path,
978 "Schema type unions must be unique",
979 ));
980 }
981 }
982 }
983 Value::Array(_) => {
984 return Err(unsupported_schema(
985 source_path,
986 "a Schema type union cannot be empty",
987 ));
988 }
989 _ => {
990 return Err(unsupported_schema(
991 source_path,
992 "Schema `type` must be a string or array",
993 ));
994 }
995 }
996 }
997
998 Ok(())
999}
1000
1001fn validate_schema_composition_keywords(
1002 object: &Map<String, Value>,
1003 source_path: &Path,
1004) -> Result<(), CodegenError> {
1005 for keyword in ["oneOf", "anyOf"] {
1006 if let Some(value) = object.get(keyword) {
1007 let Some(alternatives) = value.as_array().filter(|values| !values.is_empty()) else {
1008 return Err(unsupported_schema(
1009 source_path,
1010 format!("Schema `{keyword}` must be a non-empty array of Schemas"),
1011 ));
1012 };
1013 for alternative in alternatives {
1014 validate_schema_profile(alternative, source_path)?;
1015 }
1016 }
1017 }
1018
1019 Ok(())
1020}
1021
1022fn validate_schema_scalar_keywords(
1023 object: &Map<String, Value>,
1024 source_path: &Path,
1025) -> Result<(), CodegenError> {
1026 if let Some(values) = object.get("enum")
1027 && values.as_array().is_none_or(Vec::is_empty)
1028 {
1029 return Err(unsupported_schema(
1030 source_path,
1031 "Schema `enum` must be a non-empty array",
1032 ));
1033 }
1034 if let Some(required) = object.get("required")
1035 && required
1036 .as_array()
1037 .is_none_or(|values| values.iter().any(|value| !value.is_string()))
1038 {
1039 return Err(unsupported_schema(
1040 source_path,
1041 "Schema `required` must be an array of strings",
1042 ));
1043 }
1044 if let Some(pattern) = object.get("pattern") {
1045 let Some(pattern) = pattern.as_str() else {
1046 return Err(unsupported_schema(
1047 source_path,
1048 "Schema `pattern` must be a string",
1049 ));
1050 };
1051 compile_portable_pattern(pattern).map_err(|error| {
1052 unsupported_schema(
1053 source_path,
1054 format!("Schema `pattern` is outside the portable regex subset: {error}"),
1055 )
1056 })?;
1057 }
1058 if let Some(format) = object.get("format") {
1059 let Some(format) = format.as_str() else {
1060 return Err(unsupported_schema(
1061 source_path,
1062 "Schema `format` must be a string",
1063 ));
1064 };
1065 if !PORTABLE_STRING_FORMATS.contains(&format) {
1066 return Err(unsupported_schema(
1067 source_path,
1068 format!("format `{format}` is outside the portable value profile"),
1069 ));
1070 }
1071 }
1072 if object
1073 .get("uniqueItems")
1074 .is_some_and(|value| !value.is_boolean())
1075 {
1076 return Err(unsupported_schema(
1077 source_path,
1078 "Schema `uniqueItems` must be a boolean",
1079 ));
1080 }
1081
1082 Ok(())
1083}
1084
1085fn validate_schema_numeric_keywords(
1086 object: &Map<String, Value>,
1087 source_path: &Path,
1088) -> Result<(), CodegenError> {
1089 for keyword in [
1090 "minLength",
1091 "maxLength",
1092 "minItems",
1093 "maxItems",
1094 "minProperties",
1095 "maxProperties",
1096 ] {
1097 if let Some(value) = object.get(keyword)
1098 && non_negative_safe_schema_integer(value).is_none()
1099 {
1100 return Err(unsupported_schema(
1101 source_path,
1102 format!("Schema `{keyword}` must be a non-negative safe integer"),
1103 ));
1104 }
1105 }
1106 for keyword in ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"] {
1107 if let Some(value) = object.get(keyword)
1108 && value.as_f64().is_none_or(|number| !number.is_finite())
1109 {
1110 return Err(unsupported_schema(
1111 source_path,
1112 format!("Schema `{keyword}` must be a finite number"),
1113 ));
1114 }
1115 }
1116
1117 Ok(())
1118}
1119
1120fn validate_schema_children(
1121 object: &Map<String, Value>,
1122 source_path: &Path,
1123) -> Result<(), CodegenError> {
1124 if let Some(items) = object.get("items") {
1125 validate_schema_profile(items, source_path)?;
1126 }
1127 for keyword in ["properties", "$defs"] {
1128 if let Some(value) = object.get(keyword) {
1129 let Some(schemas) = value.as_object() else {
1130 return Err(unsupported_schema(
1131 source_path,
1132 format!("Schema `{keyword}` must be an object of Schemas"),
1133 ));
1134 };
1135 for child in schemas.values() {
1136 validate_schema_profile(child, source_path)?;
1137 }
1138 }
1139 }
1140 if let Some(additional) = object.get("additionalProperties")
1141 && !additional.is_boolean()
1142 {
1143 validate_schema_profile(additional, source_path)?;
1144 }
1145 if let Some(property_names) = object.get("propertyNames") {
1146 validate_schema_profile(property_names, source_path)?;
1147 }
1148 if object.contains_key("then") || object.contains_key("else") {
1149 let Some(condition) = object.get("if") else {
1150 return Err(unsupported_schema(
1151 source_path,
1152 "Schema `then` and `else` require an `if` Schema",
1153 ));
1154 };
1155 validate_schema_profile(condition, source_path)?;
1156 } else if let Some(condition) = object.get("if") {
1157 validate_schema_profile(condition, source_path)?;
1158 }
1159 for keyword in ["then", "else"] {
1160 if let Some(branch) = object.get(keyword) {
1161 validate_schema_profile(branch, source_path)?;
1162 }
1163 }
1164
1165 Ok(())
1166}
1167
1168fn validate_schema_type(schema_type: &str, source_path: &Path) -> Result<(), CodegenError> {
1169 if PORTABLE_SCHEMA_TYPES.contains(&schema_type) {
1170 Ok(())
1171 } else {
1172 Err(unsupported_schema(
1173 source_path,
1174 format!("Schema type `{schema_type}` is outside the portable profile"),
1175 ))
1176 }
1177}
1178
1179fn compile_portable_pattern(pattern: &str) -> Result<Regex, String> {
1187 if !has_portable_pattern_syntax(pattern) {
1188 return Err("pattern uses syntax outside the shared Rust/ECMAScript subset".to_owned());
1189 }
1190 Regex::new(pattern).map_err(|error| error.to_string())
1191}
1192
1193#[derive(Clone, Copy, Debug, Default)]
1194#[expect(
1195 clippy::struct_excessive_bools,
1196 reason = "these are independent regex risk facts mirrored explicitly in the TypeScript gate"
1197)]
1198struct PortablePatternAtom {
1199 can_match_empty: bool,
1200 has_variable_shape: bool,
1201 is_quantified: bool,
1202 prefix_can_match_empty: bool,
1203 contains_variable_repetition: bool,
1204 contains_alternation: bool,
1205 is_group: bool,
1206}
1207
1208#[derive(Debug)]
1209#[expect(
1210 clippy::struct_excessive_bools,
1211 reason = "branch-local and group-wide regex risk facts are independent parser state"
1212)]
1213struct PortablePatternFrame {
1214 sequence_can_match_empty: bool,
1215 earlier_branch_can_match_empty: bool,
1216 contains_variable_repetition: bool,
1217 contains_alternation: bool,
1218 branch_has_variable_shape: bool,
1219 last_atom: Option<PortablePatternAtom>,
1220}
1221
1222impl Default for PortablePatternFrame {
1223 fn default() -> Self {
1224 Self {
1225 sequence_can_match_empty: true,
1226 earlier_branch_can_match_empty: false,
1227 contains_variable_repetition: false,
1228 contains_alternation: false,
1229 branch_has_variable_shape: false,
1230 last_atom: None,
1231 }
1232 }
1233}
1234
1235#[derive(Clone, Copy, Debug)]
1236struct PortableRepetition {
1237 minimum: u32,
1238 maximum: Option<u32>,
1239}
1240
1241impl PortableRepetition {
1242 fn has_variable_extent(self) -> bool {
1243 self.maximum != Some(self.minimum)
1244 }
1245}
1246
1247#[expect(
1248 clippy::too_many_lines,
1249 reason = "keeping the single-pass scanner locally isomorphic with the TypeScript gate makes parity auditable"
1250)]
1251fn has_portable_pattern_syntax(pattern: &str) -> bool {
1252 if pattern.chars().count() > MAX_PORTABLE_PATTERN_CHARS
1253 || pattern.as_bytes().first() != Some(&b'^')
1254 {
1255 return false;
1256 }
1257
1258 let bytes = pattern.as_bytes();
1259 let mut index = 0;
1260 let mut in_class = false;
1261 let mut class_start = None;
1262 let mut frames = vec![PortablePatternFrame::default()];
1263 while index < bytes.len() {
1264 if in_class {
1265 match bytes[index] {
1266 b'\\' => {
1267 if pattern[index..].starts_with(PORTABLE_UNICODE_PROPERTY_ESCAPE) {
1268 index += PORTABLE_UNICODE_PROPERTY_ESCAPE.len();
1269 continue;
1270 }
1271 let Some(&escaped) = bytes.get(index + 1) else {
1272 return false;
1273 };
1274 if !matches!(
1275 escaped,
1276 b'^' | b'$'
1277 | b'\\'
1278 | b'.'
1279 | b'*'
1280 | b'+'
1281 | b'?'
1282 | b'('
1283 | b')'
1284 | b'['
1285 | b']'
1286 | b'{'
1287 | b'}'
1288 | b'|'
1289 | b'/'
1290 | b'-'
1291 ) {
1292 return false;
1293 }
1294 index += 2;
1295 }
1296 b']' => {
1297 let Some(start) = class_start.take() else {
1298 return false;
1299 };
1300 if matches!(&pattern[start..index], "" | "^") {
1301 return false;
1302 }
1303 in_class = false;
1304 let Some(frame) = frames.last_mut() else {
1305 return false;
1306 };
1307 if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1308 return false;
1309 }
1310 index += 1;
1311 }
1312 b'[' => return false,
1313 operator @ (b'&' | b'-' | b'~' | b'|')
1314 if bytes.get(index + 1) == Some(&operator) =>
1315 {
1316 return false;
1317 }
1318 byte if byte.is_ascii_control() => return false,
1319 byte if byte.is_ascii() => index += 1,
1320 _ => {
1321 let Some(character) = pattern[index..].chars().next() else {
1322 return false;
1323 };
1324 if character.is_control() {
1325 return false;
1326 }
1327 index += character.len_utf8();
1328 }
1329 }
1330 continue;
1331 }
1332
1333 match bytes[index] {
1334 b'\\' => {
1335 if pattern[index..].starts_with(PORTABLE_UNICODE_PROPERTY_ESCAPE) {
1336 index += PORTABLE_UNICODE_PROPERTY_ESCAPE.len();
1337 } else {
1338 let Some(&escaped) = bytes.get(index + 1) else {
1339 return false;
1340 };
1341 if !matches!(
1342 escaped,
1343 b'^' | b'$'
1344 | b'\\'
1345 | b'.'
1346 | b'*'
1347 | b'+'
1348 | b'?'
1349 | b'('
1350 | b')'
1351 | b'['
1352 | b']'
1353 | b'{'
1354 | b'}'
1355 | b'|'
1356 | b'/'
1357 ) {
1358 return false;
1359 }
1360 index += 2;
1361 }
1362 let Some(frame) = frames.last_mut() else {
1363 return false;
1364 };
1365 if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1366 return false;
1367 }
1368 }
1369 b'[' => {
1370 in_class = true;
1371 class_start = Some(index + 1);
1372 index += 1;
1373 }
1374 b'(' => {
1375 if bytes.get(index + 1) == Some(&b'?') {
1376 if !pattern[index..].starts_with("(?:") {
1377 return false;
1378 }
1379 index += 3;
1380 } else {
1381 index += 1;
1382 }
1383 frames.push(PortablePatternFrame::default());
1384 }
1385 b')' => {
1386 if frames.len() == 1 {
1387 return false;
1388 }
1389 let Some(group) = frames.pop() else {
1390 return false;
1391 };
1392 let can_match_empty =
1393 group.earlier_branch_can_match_empty || group.sequence_can_match_empty;
1394 if can_match_empty {
1395 return false;
1396 }
1397 let atom = PortablePatternAtom {
1398 can_match_empty,
1399 has_variable_shape: group.contains_variable_repetition
1400 || group.contains_alternation,
1401 contains_variable_repetition: group.contains_variable_repetition,
1402 contains_alternation: group.contains_alternation,
1403 is_group: true,
1404 ..PortablePatternAtom::default()
1405 };
1406 let Some(parent) = frames.last_mut() else {
1407 return false;
1408 };
1409 if !push_portable_pattern_atom(parent, atom) {
1410 return false;
1411 }
1412 index += 1;
1413 }
1414 b'|' => {
1415 if frames.len() == 1 {
1416 return false;
1417 }
1418 let Some(frame) = frames.last_mut() else {
1419 return false;
1420 };
1421 frame.contains_alternation = true;
1422 frame.earlier_branch_can_match_empty |= frame.sequence_can_match_empty;
1423 frame.sequence_can_match_empty = true;
1424 frame.branch_has_variable_shape = false;
1425 frame.last_atom = None;
1426 index += 1;
1427 }
1428 b'*' | b'+' | b'?' => {
1429 let repetition = match bytes[index] {
1430 b'*' => PortableRepetition {
1431 minimum: 0,
1432 maximum: None,
1433 },
1434 b'+' => PortableRepetition {
1435 minimum: 1,
1436 maximum: None,
1437 },
1438 b'?' => PortableRepetition {
1439 minimum: 0,
1440 maximum: Some(1),
1441 },
1442 _ => unreachable!(),
1443 };
1444 let Some(frame) = frames.last_mut() else {
1445 return false;
1446 };
1447 if !apply_portable_repetition(frame, repetition) {
1448 return false;
1449 }
1450 index += 1;
1451 }
1452 b'{' => {
1453 let Some(close_offset) = pattern[index + 1..].find('}') else {
1454 return false;
1455 };
1456 let close = index + 1 + close_offset;
1457 let Some(repetition) = parse_portable_repetition(&pattern[index + 1..close]) else {
1458 return false;
1459 };
1460 let Some(frame) = frames.last_mut() else {
1461 return false;
1462 };
1463 if !apply_portable_repetition(frame, repetition) {
1464 return false;
1465 }
1466 index = close + 1;
1467 }
1468 b'^' if index == 0 => {
1469 let Some(frame) = frames.last_mut() else {
1470 return false;
1471 };
1472 frame.last_atom = None;
1473 index += 1;
1474 }
1475 b'$' if is_portable_end_anchor(pattern, index) => {
1476 let Some(frame) = frames.last_mut() else {
1477 return false;
1478 };
1479 frame.last_atom = None;
1480 index += 1;
1481 }
1482 b']' | b'}' | b'.' | b'^' | b'$' => return false,
1483 byte if byte.is_ascii_control() => return false,
1484 byte if byte.is_ascii() => {
1485 let Some(frame) = frames.last_mut() else {
1486 return false;
1487 };
1488 if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1489 return false;
1490 }
1491 index += 1;
1492 }
1493 _ => {
1494 let Some(character) = pattern[index..].chars().next() else {
1495 return false;
1496 };
1497 if character.is_control() {
1498 return false;
1499 }
1500 let Some(frame) = frames.last_mut() else {
1501 return false;
1502 };
1503 if !push_portable_pattern_atom(frame, PortablePatternAtom::default()) {
1504 return false;
1505 }
1506 index += character.len_utf8();
1507 }
1508 }
1509 }
1510
1511 if in_class || frames.len() != 1 {
1512 return false;
1513 }
1514 let frame = &frames[0];
1515 has_portable_end_anchor(pattern)
1516 || (frame.last_atom.is_some()
1517 && !frame.contains_variable_repetition
1518 && !frame.contains_alternation)
1519}
1520
1521fn has_portable_end_anchor(pattern: &str) -> bool {
1522 let bytes = pattern.as_bytes();
1523 if bytes.last() != Some(&b'$') {
1524 return false;
1525 }
1526 bytes[..bytes.len() - 1]
1527 .iter()
1528 .rev()
1529 .take_while(|byte| **byte == b'\\')
1530 .count()
1531 % 2
1532 == 0
1533}
1534
1535fn is_portable_end_anchor(pattern: &str, index: usize) -> bool {
1536 index + 1 == pattern.len() && has_portable_end_anchor(pattern)
1537}
1538
1539fn push_portable_pattern_atom(
1540 frame: &mut PortablePatternFrame,
1541 mut atom: PortablePatternAtom,
1542) -> bool {
1543 if atom.has_variable_shape && frame.branch_has_variable_shape {
1544 return false;
1545 }
1546 atom.prefix_can_match_empty = frame.sequence_can_match_empty;
1547 frame.sequence_can_match_empty &= atom.can_match_empty;
1548 frame.branch_has_variable_shape |= atom.has_variable_shape;
1549 frame.contains_variable_repetition |= atom.contains_variable_repetition;
1550 frame.contains_alternation |= atom.contains_alternation;
1551 frame.last_atom = Some(atom);
1552 true
1553}
1554
1555fn apply_portable_repetition(
1556 frame: &mut PortablePatternFrame,
1557 repetition: PortableRepetition,
1558) -> bool {
1559 let Some(atom) = frame.last_atom.as_mut() else {
1560 return false;
1561 };
1562 if atom.is_quantified || repetition.maximum == Some(0) {
1563 return false;
1564 }
1565 if atom.is_group
1566 && (atom.can_match_empty || atom.contains_variable_repetition || atom.contains_alternation)
1567 {
1568 return false;
1569 }
1570
1571 let has_variable_extent = repetition.has_variable_extent();
1572 if has_variable_extent && frame.branch_has_variable_shape {
1573 return false;
1574 }
1575 atom.is_quantified = true;
1576 atom.has_variable_shape |= has_variable_extent;
1577 atom.can_match_empty = repetition.minimum == 0 || atom.can_match_empty;
1578 frame.sequence_can_match_empty = atom.prefix_can_match_empty && atom.can_match_empty;
1579 frame.branch_has_variable_shape |= has_variable_extent;
1580 frame.contains_variable_repetition |= has_variable_extent;
1581 true
1582}
1583
1584fn parse_portable_repetition(value: &str) -> Option<PortableRepetition> {
1585 let (minimum, maximum) = if let Some((minimum, maximum)) = value.split_once(',') {
1586 if maximum.contains(',') {
1587 return None;
1588 }
1589 let minimum = parse_portable_repetition_bound(minimum)?;
1590 let maximum = if maximum.is_empty() {
1591 None
1592 } else {
1593 Some(parse_portable_repetition_bound(maximum)?)
1594 };
1595 (minimum, maximum)
1596 } else {
1597 let minimum = parse_portable_repetition_bound(value)?;
1598 (minimum, Some(minimum))
1599 };
1600 if maximum.is_some_and(|maximum| maximum < minimum) {
1601 return None;
1602 }
1603 Some(PortableRepetition { minimum, maximum })
1604}
1605
1606fn parse_portable_repetition_bound(value: &str) -> Option<u32> {
1607 if value.is_empty()
1608 || value.len() > 1 && value.starts_with('0')
1609 || !value.bytes().all(|byte| byte.is_ascii_digit())
1610 {
1611 return None;
1612 }
1613 value
1614 .parse::<u32>()
1615 .ok()
1616 .filter(|value| *value <= MAX_PORTABLE_REPETITION)
1617}
1618
1619#[expect(
1620 clippy::cast_possible_truncation,
1621 clippy::cast_sign_loss,
1622 reason = "the finite, non-negative, integral, safe-integer checks make this conversion exact"
1623)]
1624fn non_negative_safe_schema_integer(value: &Value) -> Option<u64> {
1625 let number = value.as_f64()?;
1626 (number.is_finite()
1627 && number >= 0.0
1628 && number.fract() == 0.0
1629 && number <= 9_007_199_254_740_991.0)
1630 .then_some(number as u64)
1631}
1632
1633fn unsupported_schema(source_path: &Path, detail: impl Into<String>) -> CodegenError {
1634 CodegenError::UnsupportedSchema {
1635 path: source_path.to_path_buf(),
1636 detail: detail.into(),
1637 }
1638}
1639
1640#[allow(clippy::too_many_lines)]
1641fn validate_value_generation_schema(
1642 schema: &Value,
1643 source_path: &Path,
1644) -> Result<(), CodegenError> {
1645 let Some(object) = schema.as_object() else {
1646 return Err(CodegenError::UnsupportedSchema {
1647 path: source_path.to_path_buf(),
1648 detail: "generated value Schemas must be objects".to_owned(),
1649 });
1650 };
1651 if object.contains_key("oneOf") || object.contains_key("anyOf") {
1652 return Err(CodegenError::UnsupportedSchema {
1653 path: source_path.to_path_buf(),
1654 detail: "generated value Schemas do not support oneOf/anyOf unions".to_owned(),
1655 });
1656 }
1657 if object.contains_key("allOf") {
1658 return Err(CodegenError::UnsupportedSchema {
1659 path: source_path.to_path_buf(),
1660 detail: "generated value Schemas do not support allOf unions".to_owned(),
1661 });
1662 }
1663 if object.contains_key("const") {
1664 return Err(CodegenError::UnsupportedSchema {
1665 path: source_path.to_path_buf(),
1666 detail: "generated value Schemas do not support const fields".to_owned(),
1667 });
1668 }
1669 if let Some(types) = object.get("type").and_then(Value::as_array) {
1670 let non_null = types
1671 .iter()
1672 .filter(|schema_type| *schema_type != "null")
1673 .count();
1674 if non_null > 1 {
1675 return Err(CodegenError::UnsupportedSchema {
1676 path: source_path.to_path_buf(),
1677 detail: "generated value Schemas only support nullable type unions".to_owned(),
1678 });
1679 }
1680 let mut narrowed = object.clone();
1681 let Some(schema_type) = types.iter().find(|schema_type| *schema_type != "null") else {
1682 return Ok(());
1683 };
1684 narrowed.insert("type".to_owned(), schema_type.clone());
1685 return validate_value_generation_schema(&Value::Object(narrowed), source_path);
1686 }
1687 if let Some(values) = object.get("enum").and_then(Value::as_array) {
1688 if values.is_empty() || !values.iter().all(Value::is_string) {
1689 return Err(CodegenError::UnsupportedSchema {
1690 path: source_path.to_path_buf(),
1691 detail: "generated enum values must be non-empty strings".to_owned(),
1692 });
1693 }
1694 let mut enum_names = BTreeSet::new();
1695 for value in values.iter().filter_map(Value::as_str) {
1696 let name = pascal_case(value);
1697 if !is_generated_enum_variant(&name) || !enum_names.insert(name) {
1698 return Err(CodegenError::UnsupportedSchema {
1699 path: source_path.to_path_buf(),
1700 detail: "generated enum values collide after Rust variant generation"
1701 .to_owned(),
1702 });
1703 }
1704 }
1705 }
1706 let Some(schema_type) = object.get("type").and_then(Value::as_str) else {
1707 if object.contains_key("enum") {
1708 return Ok(());
1709 }
1710 return Err(CodegenError::UnsupportedSchema {
1711 path: source_path.to_path_buf(),
1712 detail: "generated value Schemas need a supported type or string enum".to_owned(),
1713 });
1714 };
1715 match schema_type {
1716 "object" => {
1717 let properties = object
1718 .get("properties")
1719 .and_then(Value::as_object)
1720 .cloned()
1721 .unwrap_or_default();
1722 let mut rust_names = BTreeSet::new();
1723 let mut nested_type_names = BTreeSet::new();
1724 for (name, property) in &properties {
1725 if !rust_names.insert(rust_field_name(name)) {
1726 return Err(CodegenError::UnsupportedSchema {
1727 path: source_path.to_path_buf(),
1728 detail: format!(
1729 "object properties collide after Rust field normalization near `{name}`"
1730 ),
1731 });
1732 }
1733 if !nested_type_names.insert(pascal_case(name)) {
1734 return Err(CodegenError::UnsupportedSchema {
1735 path: source_path.to_path_buf(),
1736 detail: format!(
1737 "object properties collide after nested type-name generation near `{name}`"
1738 ),
1739 });
1740 }
1741 validate_value_generation_schema(property, source_path)?;
1742 }
1743 match object.get("additionalProperties") {
1744 Some(Value::Bool(false)) => {}
1745 Some(Value::Bool(true)) | None if !properties.is_empty() => {
1746 return Err(CodegenError::UnsupportedSchema {
1747 path: source_path.to_path_buf(),
1748 detail: "open objects with declared properties would lose unknown fields"
1749 .to_owned(),
1750 });
1751 }
1752 Some(additional) if !additional.is_boolean() && !properties.is_empty() => {
1753 return Err(CodegenError::UnsupportedSchema {
1754 path: source_path.to_path_buf(),
1755 detail: "objects cannot combine declared properties with a map payload"
1756 .to_owned(),
1757 });
1758 }
1759 Some(additional) if !additional.is_boolean() => {
1760 validate_value_generation_schema(additional, source_path)?;
1761 }
1762 _ => {}
1763 }
1764 }
1765 "array" => {
1766 let items = object
1767 .get("items")
1768 .ok_or_else(|| CodegenError::UnsupportedSchema {
1769 path: source_path.to_path_buf(),
1770 detail: "generated array Schemas must declare items".to_owned(),
1771 })?;
1772 validate_value_generation_schema(items, source_path)?;
1773 }
1774 "string" | "integer" | "number" | "boolean" | "null" => {}
1775 _ => {
1776 return Err(CodegenError::UnsupportedSchema {
1777 path: source_path.to_path_buf(),
1778 detail: format!("Schema type `{schema_type}` cannot be generated"),
1779 });
1780 }
1781 }
1782 Ok(())
1783}
1784
1785fn validate_domain_error_schema(schema: &Value, source_path: &Path) -> Result<(), CodegenError> {
1786 if schema
1787 .as_object()
1788 .is_some_and(|object| object.contains_key("allOf"))
1789 {
1790 return Err(CodegenError::UnsupportedSchema {
1791 path: source_path.to_path_buf(),
1792 detail: "Domain Error Schemas do not support allOf unions".to_owned(),
1793 });
1794 }
1795 let variants = schema
1796 .get("oneOf")
1797 .and_then(Value::as_array)
1798 .filter(|variants| !variants.is_empty())
1799 .ok_or_else(|| CodegenError::UnsupportedSchema {
1800 path: source_path.to_path_buf(),
1801 detail: "Domain Error Schemas must be a non-empty oneOf union".to_owned(),
1802 })?;
1803 let mut codes = BTreeSet::new();
1804 let mut names = BTreeSet::from(["Unknown".to_owned()]);
1805 for variant in variants {
1806 if variant
1807 .as_object()
1808 .is_some_and(|object| object.contains_key("allOf"))
1809 {
1810 return Err(CodegenError::UnsupportedSchema {
1811 path: source_path.to_path_buf(),
1812 detail: "Domain Error variants do not support allOf unions".to_owned(),
1813 });
1814 }
1815 if let Some(code) = variant.get("const").and_then(Value::as_str) {
1816 let name = pascal_case(code);
1817 if !codes.insert(code.to_owned())
1818 || !names.insert(name.clone())
1819 || !is_generated_enum_variant(&name)
1820 {
1821 return Err(CodegenError::UnsupportedSchema {
1822 path: source_path.to_path_buf(),
1823 detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1824 });
1825 }
1826 continue;
1827 }
1828 let object = variant
1829 .as_object()
1830 .ok_or_else(|| CodegenError::UnsupportedSchema {
1831 path: source_path.to_path_buf(),
1832 detail: "Domain Error variants must be string consts or objects".to_owned(),
1833 })?;
1834 let properties = object
1835 .get("properties")
1836 .and_then(Value::as_object)
1837 .ok_or_else(|| CodegenError::UnsupportedSchema {
1838 path: source_path.to_path_buf(),
1839 detail: "structured Domain Error variants need properties".to_owned(),
1840 })?;
1841 if object.get("additionalProperties") != Some(&Value::Bool(false))
1842 || !required_fields(variant).contains("code")
1843 || properties
1844 .keys()
1845 .any(|name| name != "code" && name != "payload")
1846 {
1847 return Err(CodegenError::UnsupportedSchema {
1848 path: source_path.to_path_buf(),
1849 detail:
1850 "structured Domain Error objects must explicitly allow only code and payload"
1851 .to_owned(),
1852 });
1853 }
1854 let code = object
1855 .get("properties")
1856 .and_then(Value::as_object)
1857 .and_then(|properties| properties.get("code"))
1858 .and_then(|code| code.get("const"))
1859 .and_then(Value::as_str)
1860 .ok_or_else(|| CodegenError::UnsupportedSchema {
1861 path: source_path.to_path_buf(),
1862 detail: "structured Domain Error variants need a const string code".to_owned(),
1863 })?;
1864 let name = pascal_case(code);
1865 if !codes.insert(code.to_owned())
1866 || !names.insert(name.clone())
1867 || !is_generated_enum_variant(&name)
1868 {
1869 return Err(CodegenError::UnsupportedSchema {
1870 path: source_path.to_path_buf(),
1871 detail: format!("Domain Error code `{code}` is duplicated or name-colliding"),
1872 });
1873 }
1874 if let Some(payload) = object
1875 .get("properties")
1876 .and_then(Value::as_object)
1877 .and_then(|properties| properties.get("payload"))
1878 {
1879 validate_value_generation_schema(payload, source_path)?;
1880 }
1881 }
1882 Ok(())
1883}
1884
1885fn generation_input(path: &Path) -> Result<(GeneratedMetadata, ContractIr), CodegenError> {
1886 let descriptor = load_descriptor(path)?;
1887 let contract = contract_ir(&descriptor);
1888 let metadata = GeneratedMetadata {
1889 capability_id: descriptor.capability_id.clone(),
1890 descriptor_version: descriptor.version.clone(),
1891 portable: descriptor.portable,
1892 cross_lane_transfer: descriptor.cross_lane_transfer,
1893 };
1894 Ok((metadata, contract))
1895}
1896
1897pub fn generate_projection(
1899 path: &Path,
1900 language: ProjectionLanguage,
1901) -> Result<GeneratedProjection, CodegenError> {
1902 let (metadata, contract) = generation_input(path)?;
1903 let source = match language {
1904 ProjectionLanguage::Rust => generate_rust(&contract),
1905 ProjectionLanguage::TypeScript => generate_typescript(&contract),
1906 ProjectionLanguage::Wit => wit::generate_wit(&contract)?,
1907 ProjectionLanguage::RustRuntime => generate_rust_runtime(&contract)?,
1908 };
1909 Ok(GeneratedProjection {
1910 metadata,
1911 language,
1912 source,
1913 })
1914}
1915
1916pub fn generate(path: &Path) -> Result<GeneratedArtifacts, CodegenError> {
1918 let (metadata, contract) = generation_input(path)?;
1919 Ok(GeneratedArtifacts {
1920 metadata,
1921 rust: generate_rust(&contract),
1922 typescript: generate_typescript(&contract),
1923 })
1924}
1925
1926pub fn write_generated(
1928 descriptor_path: &Path,
1929 rust_path: &Path,
1930 typescript_path: &Path,
1931) -> Result<(), CodegenError> {
1932 let artifacts = generate(descriptor_path)?;
1933 write_artifact(rust_path, &artifacts.rust)?;
1934 write_artifact(typescript_path, &artifacts.typescript)?;
1935 Ok(())
1936}
1937
1938pub fn write_projection(
1940 descriptor_path: &Path,
1941 language: ProjectionLanguage,
1942 output_path: &Path,
1943) -> Result<(), CodegenError> {
1944 let projection = generate_projection(descriptor_path, language)?;
1945 write_artifact(output_path, &projection.source)
1946}
1947
1948pub fn check_generated(
1950 descriptor_path: &Path,
1951 rust_path: &Path,
1952 typescript_path: &Path,
1953) -> Result<(), CodegenError> {
1954 let artifacts = generate(descriptor_path)?;
1955 check_artifact(rust_path, &artifacts.rust)?;
1956 check_artifact(typescript_path, &artifacts.typescript)?;
1957 Ok(())
1958}
1959
1960pub fn check_projection(
1962 descriptor_path: &Path,
1963 language: ProjectionLanguage,
1964 output_path: &Path,
1965) -> Result<(), CodegenError> {
1966 let projection = generate_projection(descriptor_path, language)?;
1967 check_artifact(output_path, &projection.source)
1968}
1969
1970pub fn round_trip_portable_json(value: &Value) -> Result<Value, CodegenError> {
1976 validate_portable_value(value, "$".to_owned())?;
1977 let encoded = serde_json::to_vec(value).expect("JSON values are serializable");
1978 serde_json::from_slice(&encoded).map_err(|error| CodegenError::InvalidPortableValue {
1979 path: "$".to_owned(),
1980 detail: format!("wire JSON could not be decoded: {error}"),
1981 })
1982}
1983
1984pub fn validate_wire_value(schema_path: &Path, value: &Value) -> Result<(), CodegenError> {
1992 let schema_path = canonical_path(schema_path)?;
1993 let package_root = schema_path
1994 .parent()
1995 .unwrap_or_else(|| Path::new("."))
1996 .to_path_buf();
1997 let source = read_json(&schema_path)?;
1998 let mut ref_stack = Vec::new();
1999 let schema = resolve_refs(
2000 &source,
2001 &source,
2002 &schema_path,
2003 &package_root,
2004 &mut ref_stack,
2005 )?;
2006 validate_portable_value(value, "$".to_owned())?;
2007 validate_schema_profile(&schema, &schema_path)?;
2008 validate_wire_value_inner(&schema, value, "$", &schema_path)
2009}
2010
2011fn validate_wire_value_inner(
2012 schema: &Value,
2013 value: &Value,
2014 path: &str,
2015 source_path: &Path,
2016) -> Result<(), CodegenError> {
2017 if let Some(constant) = schema.get("const")
2018 && !portable_json_semantic_equal(value, constant)
2019 {
2020 return invalid_wire_value(path, "value does not match the Schema const", source_path);
2021 }
2022 if let Some(values) = schema.get("enum").and_then(Value::as_array)
2023 && !values
2024 .iter()
2025 .any(|candidate| portable_json_semantic_equal(candidate, value))
2026 {
2027 return invalid_wire_value(
2028 path,
2029 "value is not included in the Schema enum",
2030 source_path,
2031 );
2032 }
2033
2034 if let Some(alternatives) = schema.get("oneOf").and_then(Value::as_array) {
2035 let matches = alternatives
2036 .iter()
2037 .filter(|alternative| {
2038 validate_wire_value_inner(alternative, value, path, source_path).is_ok()
2039 })
2040 .count();
2041 if matches == 0
2042 && is_open_domain_error_schema(schema)
2043 && is_unknown_domain_error_wire(schema, value)
2044 {
2045 return validate_portable_value(value, path.to_owned());
2046 }
2047 if matches != 1 {
2048 return invalid_wire_value(
2049 path,
2050 "value must match exactly one Schema oneOf alternative",
2051 source_path,
2052 );
2053 }
2054 }
2055 if let Some(alternatives) = schema.get("anyOf").and_then(Value::as_array)
2056 && !alternatives.iter().any(|alternative| {
2057 validate_wire_value_inner(alternative, value, path, source_path).is_ok()
2058 })
2059 {
2060 return invalid_wire_value(
2061 path,
2062 "value must match at least one Schema anyOf alternative",
2063 source_path,
2064 );
2065 }
2066 if let Some(condition) = schema.get("if") {
2067 let branch = if validate_wire_value_inner(condition, value, path, source_path).is_ok() {
2068 schema.get("then")
2069 } else {
2070 schema.get("else")
2071 };
2072 if let Some(branch) = branch {
2073 validate_wire_value_inner(branch, value, path, source_path)?;
2074 }
2075 }
2076
2077 match schema.get("type") {
2078 Some(Value::String(schema_type)) => {
2079 validate_wire_type(schema, schema_type, value, path, source_path)
2080 }
2081 Some(Value::Array(types)) => {
2082 let mut errors = Vec::new();
2083 for schema_type in types {
2084 let Some(schema_type) = schema_type.as_str() else {
2085 continue;
2086 };
2087 match validate_wire_type(schema, schema_type, value, path, source_path) {
2088 Ok(()) => return Ok(()),
2089 Err(error) => errors.push(error.to_string()),
2090 }
2091 }
2092 invalid_wire_value(
2093 path,
2094 &format!(
2095 "value does not match any Schema type ({})",
2096 errors.join("; ")
2097 ),
2098 source_path,
2099 )
2100 }
2101 Some(_) => invalid_wire_value(path, "Schema type must be a string or array", source_path),
2102 None if schema.as_object().is_some_and(|object| {
2103 object.contains_key("properties")
2104 || object.contains_key("required")
2105 || object.contains_key("additionalProperties")
2106 }) =>
2107 {
2108 validate_wire_type(schema, "object", value, path, source_path)
2109 }
2110 None if schema
2111 .as_object()
2112 .is_some_and(|object| object.contains_key("const") || object.contains_key("enum")) =>
2113 {
2114 Ok(())
2115 }
2116 None => Ok(()),
2117 }
2118}
2119
2120fn is_open_domain_error_schema(schema: &Value) -> bool {
2121 schema
2122 .get("oneOf")
2123 .and_then(Value::as_array)
2124 .is_some_and(|variants| {
2125 !variants.is_empty()
2126 && variants.iter().all(|variant| {
2127 variant.get("const").and_then(Value::as_str).is_some()
2128 || variant
2129 .get("properties")
2130 .and_then(Value::as_object)
2131 .and_then(|properties| properties.get("code"))
2132 .and_then(|code| code.get("const"))
2133 .and_then(Value::as_str)
2134 .is_some()
2135 })
2136 })
2137}
2138
2139fn is_unknown_domain_error_wire(schema: &Value, value: &Value) -> bool {
2140 let known_codes = schema
2141 .get("oneOf")
2142 .and_then(Value::as_array)
2143 .into_iter()
2144 .flatten()
2145 .filter_map(|variant| {
2146 variant.get("const").and_then(Value::as_str).or_else(|| {
2147 variant
2148 .get("properties")
2149 .and_then(Value::as_object)
2150 .and_then(|properties| properties.get("code"))
2151 .and_then(|code| code.get("const"))
2152 .and_then(Value::as_str)
2153 })
2154 })
2155 .collect::<BTreeSet<_>>();
2156 match value {
2157 Value::String(code) => !known_codes.contains(code.as_str()),
2158 Value::Object(object) => object
2159 .get("code")
2160 .and_then(Value::as_str)
2161 .is_some_and(|code| !known_codes.contains(code)),
2162 _ => false,
2163 }
2164}
2165
2166fn validate_wire_type(
2167 schema: &Value,
2168 schema_type: &str,
2169 value: &Value,
2170 path: &str,
2171 source_path: &Path,
2172) -> Result<(), CodegenError> {
2173 match schema_type {
2174 "object" => validate_wire_object(schema, value, path, source_path),
2175 "array" => validate_wire_array(schema, value, path, source_path),
2176 "string" => validate_wire_string(schema, value, path, source_path),
2177 "integer" => {
2178 if !value.is_number()
2179 || value.as_i64().is_none() && value.as_u64().is_none()
2180 || !is_safe_json_integer(value)
2181 {
2182 return invalid_wire_value(path, "expected an integer", source_path);
2183 }
2184 validate_numeric_constraint(schema, value, path, source_path)
2185 }
2186 "number" => {
2187 if value
2188 .as_number()
2189 .is_none_or(|number| !is_safe_json_number(number))
2190 {
2191 return invalid_wire_value(path, "expected a finite number", source_path);
2192 }
2193 validate_numeric_constraint(schema, value, path, source_path)
2194 }
2195 "boolean" => {
2196 if !value.is_boolean() {
2197 return invalid_wire_value(path, "expected a boolean", source_path);
2198 }
2199 Ok(())
2200 }
2201 "null" => {
2202 if !value.is_null() {
2203 return invalid_wire_value(path, "expected null", source_path);
2204 }
2205 Ok(())
2206 }
2207 _ => invalid_wire_value(
2208 path,
2209 "Schema type is outside the portable profile",
2210 source_path,
2211 ),
2212 }
2213}
2214
2215fn is_safe_json_integer(value: &Value) -> bool {
2216 value
2217 .as_i64()
2218 .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
2219 || value
2220 .as_u64()
2221 .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
2222}
2223
2224fn is_safe_json_number(number: &serde_json::Number) -> bool {
2225 number
2226 .as_i64()
2227 .is_some_and(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value))
2228 || number
2229 .as_u64()
2230 .is_some_and(|value| value <= MAX_SAFE_INTEGER as u64)
2231 || number.as_f64().is_some_and(|value| {
2232 value.is_finite() && (value.abs() <= 9_007_199_254_740_991.0 || value.fract() != 0.0)
2233 })
2234}
2235
2236fn validate_wire_object(
2237 schema: &Value,
2238 value: &Value,
2239 path: &str,
2240 source_path: &Path,
2241) -> Result<(), CodegenError> {
2242 let Value::Object(properties) = value else {
2243 return invalid_wire_value(path, "expected an object", source_path);
2244 };
2245 let property_count = u64::try_from(properties.len()).unwrap_or(u64::MAX);
2246 if let Some(minimum) = schema
2247 .get("minProperties")
2248 .and_then(non_negative_safe_schema_integer)
2249 && property_count < minimum
2250 {
2251 return invalid_wire_value(
2252 path,
2253 "object has fewer properties than minProperties",
2254 source_path,
2255 );
2256 }
2257 if let Some(maximum) = schema
2258 .get("maxProperties")
2259 .and_then(non_negative_safe_schema_integer)
2260 && property_count > maximum
2261 {
2262 return invalid_wire_value(
2263 path,
2264 "object has more properties than maxProperties",
2265 source_path,
2266 );
2267 }
2268 let required = required_fields(schema);
2269 for field in required {
2270 if !properties.contains_key(&field) {
2271 return invalid_wire_value(
2272 &format!("{path}.{field}"),
2273 "required field is missing",
2274 source_path,
2275 );
2276 }
2277 }
2278 let declared = schema
2279 .get("properties")
2280 .and_then(Value::as_object)
2281 .cloned()
2282 .unwrap_or_default();
2283 for (field, field_value) in properties {
2284 if let Some(property_names) = schema.get("propertyNames") {
2285 validate_wire_value_inner(
2286 property_names,
2287 &Value::String(field.clone()),
2288 &format!("{path}.{field}"),
2289 source_path,
2290 )?;
2291 }
2292 if let Some(field_schema) = declared.get(field) {
2293 validate_wire_value_inner(
2294 field_schema,
2295 field_value,
2296 &format!("{path}.{field}"),
2297 source_path,
2298 )?;
2299 continue;
2300 }
2301 match schema.get("additionalProperties") {
2302 Some(Value::Bool(false)) => {
2303 return invalid_wire_value(
2304 &format!("{path}.{field}"),
2305 "additional property is not allowed",
2306 source_path,
2307 );
2308 }
2309 Some(additional_schema) if !additional_schema.is_boolean() => {
2310 validate_wire_value_inner(
2311 additional_schema,
2312 field_value,
2313 &format!("{path}.{field}"),
2314 source_path,
2315 )?;
2316 }
2317 _ => {}
2318 }
2319 }
2320 Ok(())
2321}
2322
2323fn validate_wire_array(
2324 schema: &Value,
2325 value: &Value,
2326 path: &str,
2327 source_path: &Path,
2328) -> Result<(), CodegenError> {
2329 let Value::Array(values) = value else {
2330 return invalid_wire_value(path, "expected an array", source_path);
2331 };
2332 let length = u64::try_from(values.len()).unwrap_or(u64::MAX);
2333 if let Some(minimum) = schema
2334 .get("minItems")
2335 .and_then(non_negative_safe_schema_integer)
2336 && length < minimum
2337 {
2338 return invalid_wire_value(path, "array has fewer items than minItems", source_path);
2339 }
2340 if let Some(maximum) = schema
2341 .get("maxItems")
2342 .and_then(non_negative_safe_schema_integer)
2343 && length > maximum
2344 {
2345 return invalid_wire_value(path, "array has more items than maxItems", source_path);
2346 }
2347 if schema.get("uniqueItems") == Some(&Value::Bool(true)) {
2348 let mut fingerprints = HashSet::with_capacity(values.len());
2349 for value in values {
2350 if !fingerprints.insert(portable_json_fingerprint(value)) {
2351 return invalid_wire_value(path, "array items must be unique", source_path);
2352 }
2353 }
2354 }
2355 if let Some(items) = schema.get("items") {
2356 for (index, item) in values.iter().enumerate() {
2357 validate_wire_value_inner(items, item, &format!("{path}[{index}]"), source_path)?;
2358 }
2359 }
2360 Ok(())
2361}
2362
2363fn portable_json_fingerprint(value: &Value) -> String {
2364 let mut output = String::new();
2365 write_portable_json_fingerprint(value, &mut output);
2366 output
2367}
2368
2369fn portable_json_semantic_equal(left: &Value, right: &Value) -> bool {
2370 portable_json_fingerprint(left) == portable_json_fingerprint(right)
2371}
2372
2373fn write_portable_json_fingerprint(value: &Value, output: &mut String) {
2374 match value {
2375 Value::Null => output.push('n'),
2376 Value::Bool(value) => output.push_str(if *value { "b1" } else { "b0" }),
2377 Value::Number(value) => {
2378 let number = value
2379 .as_f64()
2380 .expect("portable JSON numbers are representable as f64");
2381 let bits = if number == 0.0 { 0 } else { number.to_bits() };
2382 write!(output, "d{bits:016x};").expect("writing to a String cannot fail");
2383 }
2384 Value::String(value) => {
2385 write!(output, "s{}:", value.len()).expect("writing to a String cannot fail");
2386 output.push_str(value);
2387 }
2388 Value::Array(values) => {
2389 write!(output, "a{}[", values.len()).expect("writing to a String cannot fail");
2390 for value in values {
2391 write_portable_json_fingerprint(value, output);
2392 }
2393 output.push(']');
2394 }
2395 Value::Object(values) => {
2396 let mut entries = values.iter().collect::<Vec<_>>();
2397 entries.sort_unstable_by_key(|(key, _)| *key);
2398 write!(output, "o{}{{", entries.len()).expect("writing to a String cannot fail");
2399 for (key, value) in entries {
2400 write!(output, "k{}:", key.len()).expect("writing to a String cannot fail");
2401 output.push_str(key);
2402 write_portable_json_fingerprint(value, output);
2403 }
2404 output.push('}');
2405 }
2406 }
2407}
2408
2409fn validate_wire_string(
2410 schema: &Value,
2411 value: &Value,
2412 path: &str,
2413 source_path: &Path,
2414) -> Result<(), CodegenError> {
2415 let Value::String(value) = value else {
2416 return invalid_wire_value(path, "expected a string", source_path);
2417 };
2418 validate_string_format(schema, value, path, source_path)?;
2419 if let Some(pattern) = schema.get("pattern").and_then(Value::as_str) {
2420 let Ok(pattern) = compile_portable_pattern(pattern) else {
2421 return invalid_wire_value(path, "Schema pattern could not be compiled", source_path);
2422 };
2423 if !pattern.is_match(value) {
2424 return invalid_wire_value(path, "string does not match pattern", source_path);
2425 }
2426 }
2427 let length = u64::try_from(value.chars().count()).unwrap_or(u64::MAX);
2428 if let Some(minimum) = schema
2429 .get("minLength")
2430 .and_then(non_negative_safe_schema_integer)
2431 && length < minimum
2432 {
2433 return invalid_wire_value(path, "string is shorter than minLength", source_path);
2434 }
2435 if let Some(maximum) = schema
2436 .get("maxLength")
2437 .and_then(non_negative_safe_schema_integer)
2438 && length > maximum
2439 {
2440 return invalid_wire_value(path, "string is longer than maxLength", source_path);
2441 }
2442 Ok(())
2443}
2444
2445fn validate_numeric_constraint(
2446 schema: &Value,
2447 value: &Value,
2448 path: &str,
2449 source_path: &Path,
2450) -> Result<(), CodegenError> {
2451 let Some(number) = value.as_f64() else {
2452 return Ok(());
2453 };
2454 if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64)
2455 && number < minimum
2456 {
2457 return invalid_wire_value(path, "number is below minimum", source_path);
2458 }
2459 if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64)
2460 && number > maximum
2461 {
2462 return invalid_wire_value(path, "number is above maximum", source_path);
2463 }
2464 if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64)
2465 && number <= minimum
2466 {
2467 return invalid_wire_value(path, "number is not above exclusiveMinimum", source_path);
2468 }
2469 if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64)
2470 && number >= maximum
2471 {
2472 return invalid_wire_value(path, "number is not below exclusiveMaximum", source_path);
2473 }
2474 Ok(())
2475}
2476
2477fn validate_string_format(
2478 schema: &Value,
2479 value: &str,
2480 path: &str,
2481 source_path: &Path,
2482) -> Result<(), CodegenError> {
2483 let Some(format) = schema.get("format").and_then(Value::as_str) else {
2484 return Ok(());
2485 };
2486 let valid = match format {
2487 "int64" => is_signed_decimal(value),
2488 "uint64" => is_unsigned_decimal(value),
2489 "byte" => is_base64(value),
2490 "date-time" => is_rfc3339(value),
2491 "duration" => is_iso8601_duration(value),
2492 _ => false,
2493 };
2494 if valid {
2495 Ok(())
2496 } else {
2497 invalid_wire_value(
2498 path,
2499 &format!("string does not match portable `{format}` format"),
2500 source_path,
2501 )
2502 }
2503}
2504
2505fn invalid_wire_value<T>(path: &str, detail: &str, _source_path: &Path) -> Result<T, CodegenError> {
2506 Err(CodegenError::InvalidPortableValue {
2507 path: path.to_owned(),
2508 detail: detail.to_owned(),
2509 })
2510}
2511
2512fn is_signed_decimal(value: &str) -> bool {
2513 let digits = value.strip_prefix('-').unwrap_or(value);
2514 !digits.is_empty()
2515 && (digits == "0" || !digits.starts_with('0'))
2516 && digits.chars().all(|character| character.is_ascii_digit())
2517 && value.parse::<i64>().is_ok()
2518}
2519
2520fn is_unsigned_decimal(value: &str) -> bool {
2521 !value.is_empty()
2522 && (value == "0" || !value.starts_with('0'))
2523 && value.chars().all(|character| character.is_ascii_digit())
2524 && value.parse::<u64>().is_ok()
2525}
2526
2527fn is_base64(value: &str) -> bool {
2528 let bytes = value.as_bytes();
2529 if bytes.is_empty() {
2530 return true;
2531 }
2532 if !bytes.len().is_multiple_of(4) {
2533 return false;
2534 }
2535 let padding = bytes.iter().rev().take_while(|byte| **byte == b'=').count();
2536 if padding > 2 || bytes[..bytes.len() - padding].contains(&b'=') {
2537 return false;
2538 }
2539 for byte in &bytes[..bytes.len() - padding] {
2540 if base64_digit(*byte).is_none() {
2541 return false;
2542 }
2543 }
2544 let last = &bytes[bytes.len() - 4..];
2545 if padding == 1 {
2546 base64_digit(last[2]).is_some_and(|digit| digit.trailing_zeros() >= 2)
2547 } else if padding == 2 {
2548 base64_digit(last[1]).is_some_and(|digit| digit.trailing_zeros() >= 4)
2549 } else {
2550 base64_digit(last[2]).is_some() && base64_digit(last[3]).is_some()
2551 }
2552}
2553
2554fn base64_digit(byte: u8) -> Option<u8> {
2555 match byte {
2556 b'A'..=b'Z' => Some(byte - b'A'),
2557 b'a'..=b'z' => Some(byte - b'a' + 26),
2558 b'0'..=b'9' => Some(byte - b'0' + 52),
2559 b'+' => Some(62),
2560 b'/' => Some(63),
2561 _ => None,
2562 }
2563}
2564
2565fn is_rfc3339(value: &str) -> bool {
2566 let bytes = value.as_bytes();
2567 if bytes.len() < 20
2568 || !matches!(bytes.get(4), Some(b'-'))
2569 || !matches!(bytes.get(7), Some(b'-'))
2570 || !matches!(bytes.get(10), Some(b'T' | b't'))
2571 || !matches!(bytes.get(13), Some(b':'))
2572 || !matches!(bytes.get(16), Some(b':'))
2573 {
2574 return false;
2575 }
2576 let Some(month) = fixed_digits(bytes, 5, 2) else {
2577 return false;
2578 };
2579 let Some(day) = fixed_digits(bytes, 8, 2) else {
2580 return false;
2581 };
2582 let Some(hour) = fixed_digits(bytes, 11, 2) else {
2583 return false;
2584 };
2585 let Some(minute) = fixed_digits(bytes, 14, 2) else {
2586 return false;
2587 };
2588 let Some(second) = fixed_digits(bytes, 17, 2) else {
2589 return false;
2590 };
2591 let Some(year) = fixed_digits(bytes, 0, 4) else {
2592 return false;
2593 };
2594 let max_day = match month {
2595 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2596 4 | 6 | 9 | 11 => 30,
2597 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
2598 2 => 28,
2599 _ => return false,
2600 };
2601 if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
2602 return false;
2603 }
2604 let mut index = 19;
2605 if bytes.get(index) == Some(&b'.') {
2606 index += 1;
2607 let start = index;
2608 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2609 index += 1;
2610 }
2611 if index == start {
2612 return false;
2613 }
2614 }
2615 match bytes.get(index) {
2616 Some(b'Z' | b'z') => index + 1 == bytes.len(),
2617 Some(b'+' | b'-') => {
2618 index += 1;
2619 let Some(offset_hour) = fixed_digits(bytes, index, 2) else {
2620 return false;
2621 };
2622 index += 2;
2623 if bytes.get(index) != Some(&b':') {
2624 return false;
2625 }
2626 index += 1;
2627 let Some(offset_minute) = fixed_digits(bytes, index, 2) else {
2628 return false;
2629 };
2630 offset_hour <= 23 && offset_minute <= 59 && index + 2 == bytes.len()
2631 }
2632 _ => false,
2633 }
2634}
2635
2636fn fixed_digits(bytes: &[u8], start: usize, length: usize) -> Option<u32> {
2637 let slice = bytes.get(start..start + length)?;
2638 slice.iter().try_fold(0_u32, |value, digit| {
2639 digit
2640 .is_ascii_digit()
2641 .then(|| value * 10 + u32::from(digit - b'0'))
2642 })
2643}
2644
2645fn is_iso8601_duration(value: &str) -> bool {
2646 let bytes = value.as_bytes();
2647 let mut index = usize::from(bytes.first() == Some(&b'-'));
2648 if bytes.get(index) != Some(&b'P') {
2649 return false;
2650 }
2651 index += 1;
2652 let mut in_time = false;
2653 let mut saw_component = false;
2654 let mut saw_time_component = false;
2655 let mut last_date_order = None;
2656 let mut last_time_order = None;
2657 let mut saw_week = false;
2658 let mut saw_other_date_unit = false;
2659 while index < bytes.len() {
2660 if bytes[index] == b'T' {
2661 if in_time || index + 1 == bytes.len() {
2662 return false;
2663 }
2664 in_time = true;
2665 index += 1;
2666 continue;
2667 }
2668 let digit_start = index;
2669 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2670 index += 1;
2671 }
2672 if index == digit_start {
2673 return false;
2674 }
2675
2676 let mut fractional = false;
2677 if bytes.get(index) == Some(&b'.') {
2678 fractional = true;
2679 index += 1;
2680 let fraction_start = index;
2681 while bytes.get(index).is_some_and(u8::is_ascii_digit) {
2682 index += 1;
2683 }
2684 if index == fraction_start {
2685 return false;
2686 }
2687 }
2688
2689 let Some(&unit) = bytes.get(index) else {
2690 return false;
2691 };
2692 if in_time {
2693 let order = match unit {
2694 b'H' => 0,
2695 b'M' => 1,
2696 b'S' => 2,
2697 _ => return false,
2698 };
2699 if last_time_order.is_some_and(|previous| order <= previous) {
2700 return false;
2701 }
2702 last_time_order = Some(order);
2703 saw_time_component = true;
2704 } else {
2705 let order = match unit {
2706 b'Y' => 0,
2707 b'M' => 1,
2708 b'W' => 2,
2709 b'D' => 3,
2710 _ => return false,
2711 };
2712 if last_date_order.is_some_and(|previous| order <= previous) {
2713 return false;
2714 }
2715 last_date_order = Some(order);
2716 if unit == b'W' {
2717 if saw_other_date_unit || index + 1 != bytes.len() {
2718 return false;
2719 }
2720 saw_week = true;
2721 } else {
2722 if saw_week {
2723 return false;
2724 }
2725 saw_other_date_unit = true;
2726 }
2727 }
2728 saw_component = true;
2729 index += 1;
2730 if fractional && index != bytes.len() {
2731 return false;
2732 }
2733 }
2734 saw_component && (!in_time || saw_time_component)
2735}
2736
2737fn validate_portable_value(value: &Value, path: String) -> Result<(), CodegenError> {
2738 match value {
2739 Value::Number(number) => {
2740 if !is_safe_json_number(number) {
2741 return Err(CodegenError::InvalidPortableValue {
2742 path,
2743 detail: "wide integers must use an explicit decimal-string format".to_owned(),
2744 });
2745 }
2746 }
2747 Value::Array(values) => {
2748 for (index, value) in values.iter().enumerate() {
2749 validate_portable_value(value, format!("{path}[{index}]"))?;
2750 }
2751 }
2752 Value::Object(values) => {
2753 for (key, value) in values {
2754 validate_portable_value(value, format!("{path}.{key}"))?;
2755 }
2756 }
2757 Value::Null | Value::Bool(_) | Value::String(_) => {}
2758 }
2759 Ok(())
2760}
2761
2762fn write_artifact(path: &Path, contents: &str) -> Result<(), CodegenError> {
2763 if let Some(parent) = path.parent() {
2764 fs::create_dir_all(parent).map_err(|source| CodegenError::Io {
2765 path: parent.to_path_buf(),
2766 source,
2767 })?;
2768 }
2769 fs::write(path, contents).map_err(|source| CodegenError::Io {
2770 path: path.to_path_buf(),
2771 source,
2772 })
2773}
2774
2775fn check_artifact(path: &Path, expected: &str) -> Result<(), CodegenError> {
2776 let actual = fs::read_to_string(path).map_err(|source| CodegenError::Io {
2777 path: path.to_path_buf(),
2778 source,
2779 })?;
2780 if actual == expected {
2781 Ok(())
2782 } else {
2783 Err(CodegenError::GeneratedArtifactDrift {
2784 path: path.to_path_buf(),
2785 })
2786 }
2787}
2788
2789pub fn lint_compatibility(old_path: &Path, new_path: &Path) -> Result<bool, CompatibilityError> {
2792 let old = load_descriptor(old_path).map_err(|error| CompatibilityError::BreakingChanges {
2793 changes: vec![error.to_string()],
2794 })?;
2795 let new = load_descriptor(new_path).map_err(|error| CompatibilityError::BreakingChanges {
2796 changes: vec![error.to_string()],
2797 })?;
2798 if old.capability_id != new.capability_id {
2799 return Err(CompatibilityError::IdentityChanged {
2800 from: old.capability_id,
2801 to: new.capability_id,
2802 });
2803 }
2804 if new.parsed_version <= old.parsed_version {
2805 return Err(CompatibilityError::VersionNotAdvanced {
2806 from: old.version,
2807 to: new.version,
2808 });
2809 }
2810 if new.parsed_version.major() != old.parsed_version.major() {
2811 return Err(CompatibilityError::BreakingChanges {
2812 changes: vec!["a breaking change must use a new Capability @major identity".to_owned()],
2813 });
2814 }
2815
2816 let mut changes = Vec::new();
2817 if old.portable != new.portable {
2818 changes.push("Descriptor portability changed".to_owned());
2819 }
2820 if old.cross_lane_transfer && !new.cross_lane_transfer {
2821 changes.push("Descriptor cross-lane transfer support was removed".to_owned());
2822 }
2823 let old_operations: BTreeMap<_, _> = old
2824 .operations
2825 .iter()
2826 .map(|operation| (operation.name.as_str(), operation))
2827 .collect();
2828 let new_operations: BTreeMap<_, _> = new
2829 .operations
2830 .iter()
2831 .map(|operation| (operation.name.as_str(), operation))
2832 .collect();
2833
2834 for (name, old_operation) in &old_operations {
2835 let Some(new_operation) = new_operations.get(name) else {
2836 changes.push(format!("Operation `{name}` was removed"));
2837 continue;
2838 };
2839 if old_operation.interaction != new_operation.interaction {
2840 changes.push(format!("Operation `{name}` changed interaction"));
2841 }
2842 compare_schema(
2843 &old_operation.request_schema,
2844 &new_operation.request_schema,
2845 &format!("Operation `{name}` request"),
2846 &mut changes,
2847 );
2848 compare_schema(
2849 &old_operation.response_schema,
2850 &new_operation.response_schema,
2851 &format!("Operation `{name}` response"),
2852 &mut changes,
2853 );
2854 compare_error_schema(
2855 &old_operation.domain_error_schema,
2856 &new_operation.domain_error_schema,
2857 &format!("Operation `{name}` Domain Error"),
2858 &mut changes,
2859 );
2860 }
2861
2862 if new.parsed_version.minor() == old.parsed_version.minor() {
2863 if contract_signature(&old) != contract_signature(&new) {
2864 changes.push("a patch release changed the observable contract".to_owned());
2865 }
2866 for name in new_operations
2867 .keys()
2868 .filter(|name| !old_operations.contains_key(**name))
2869 {
2870 changes.push(format!("Operation `{name}` was added in a patch release"));
2871 }
2872 }
2873 if new.parsed_version.minor() < old.parsed_version.minor() {
2874 changes.push("Descriptor minor version moved backwards".to_owned());
2875 }
2876
2877 if changes.is_empty() {
2878 Ok(true)
2879 } else {
2880 Err(CompatibilityError::BreakingChanges { changes })
2881 }
2882}
2883
2884fn contract_signature(descriptor: &Descriptor) -> String {
2885 let operations = descriptor
2886 .operations
2887 .iter()
2888 .map(|operation| {
2889 serde_json::json!({
2890 "name": operation.name,
2891 "interaction": operation.interaction,
2892 "request": operation.request_schema,
2893 "response": operation.response_schema,
2894 "domain_error": canonical_domain_error_schema(&operation.domain_error_schema),
2895 })
2896 })
2897 .collect::<Vec<_>>();
2898 canonical_json(&Value::Array(operations))
2899}
2900
2901fn canonical_domain_error_schema(schema: &Value) -> Value {
2902 let Some(variants) = schema.get("oneOf").and_then(Value::as_array) else {
2903 return schema.clone();
2904 };
2905 let mut variants = variants.clone();
2906 variants.sort_by(|left, right| {
2907 error_variant_code(left)
2908 .unwrap_or_default()
2909 .cmp(error_variant_code(right).unwrap_or_default())
2910 });
2911 let mut schema = schema.as_object().cloned().unwrap_or_default();
2912 schema.insert("oneOf".to_owned(), Value::Array(variants));
2913 Value::Object(schema)
2914}
2915
2916fn error_variant_code(variant: &Value) -> Option<&str> {
2917 variant.get("const").and_then(Value::as_str).or_else(|| {
2918 variant
2919 .get("properties")
2920 .and_then(Value::as_object)
2921 .and_then(|properties| properties.get("code"))
2922 .and_then(|code| code.get("const"))
2923 .and_then(Value::as_str)
2924 })
2925}
2926
2927fn compare_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2928 let old_nullable = is_nullable(old);
2929 let new_nullable = is_nullable(new);
2930 if old_nullable != new_nullable {
2931 changes.push(format!("{location} changed nullability"));
2932 return;
2933 }
2934 let old_type = schema_type_name(old);
2935 let new_type = schema_type_name(new);
2936 if old_type != new_type {
2937 changes.push(format!("{location} changed type"));
2938 return;
2939 }
2940 match old_type {
2941 Some("object") => {
2942 let old_properties = old.get("properties").and_then(Value::as_object);
2943 let new_properties = new.get("properties").and_then(Value::as_object);
2944 let (Some(old_properties), Some(new_properties)) = (old_properties, new_properties)
2945 else {
2946 if canonical_json(old) != canonical_json(new) {
2947 changes.push(format!("{location} changed object shape"));
2948 }
2949 return;
2950 };
2951 let old_required = required_fields(old);
2952 let new_required = required_fields(new);
2953 for name in old_properties.keys() {
2954 let Some(new_schema) = new_properties.get(name) else {
2955 changes.push(format!("{location} field `{name}` was removed"));
2956 continue;
2957 };
2958 compare_schema(
2959 &old_properties[name],
2960 new_schema,
2961 &format!("{location} field `{name}`"),
2962 changes,
2963 );
2964 }
2965 for name in new_required.difference(&old_required) {
2966 changes.push(format!("{location} field `{name}` became required"));
2967 }
2968 for name in old_required.difference(&new_required) {
2969 changes.push(format!("{location} field `{name}` became optional"));
2970 }
2971 if old.get("additionalProperties") != new.get("additionalProperties") {
2972 changes.push(format!("{location} changed additional-properties policy"));
2973 }
2974 if schema_constraints_signature(old) != schema_constraints_signature(new) {
2975 changes.push(format!("{location} changed constraints"));
2976 }
2977 }
2978 Some("array") => {
2979 if let (Some(old_items), Some(new_items)) = (old.get("items"), new.get("items")) {
2980 compare_schema(old_items, new_items, &format!("{location} items"), changes);
2981 }
2982 if schema_constraints_signature(old) != schema_constraints_signature(new) {
2983 changes.push(format!("{location} changed constraints"));
2984 }
2985 }
2986 _ => {
2987 if canonical_json(old) != canonical_json(new) {
2988 changes.push(format!("{location} changed format or constraints"));
2989 }
2990 }
2991 }
2992}
2993
2994fn compare_error_schema(old: &Value, new: &Value, location: &str, changes: &mut Vec<String>) {
2995 let old_variants = error_variant_values(old);
2996 let new_variants = error_variant_values(new);
2997 if old_variants.is_empty() || new_variants.is_empty() {
2998 if canonical_json(old) != canonical_json(new) {
2999 changes.push(format!("{location} changed shape"));
3000 }
3001 return;
3002 }
3003 for (code, schema) in old_variants {
3004 match new_variants.get(&code) {
3005 None => changes.push(format!("{location} variant `{code}` was removed")),
3006 Some(new_schema) => compare_schema(
3007 &schema,
3008 new_schema,
3009 &format!("{location} variant `{code}`"),
3010 changes,
3011 ),
3012 }
3013 }
3014}
3015
3016fn error_variant_ir_definitions(schema: &Value) -> Vec<ErrorVariantIr> {
3017 let mut variants = schema
3018 .get("oneOf")
3019 .and_then(Value::as_array)
3020 .into_iter()
3021 .flatten()
3022 .filter_map(|variant| {
3023 if let Some(code) = variant.get("const").and_then(Value::as_str) {
3024 return Some(ErrorVariantIr {
3025 code: code.to_owned(),
3026 name: pascal_case(code),
3027 structured: false,
3028 payload: None,
3029 payload_required: false,
3030 });
3031 }
3032 let object = variant.as_object()?;
3033 let code = object
3034 .get("properties")
3035 .and_then(Value::as_object)
3036 .and_then(|properties| properties.get("code"))
3037 .and_then(|code| code.get("const"))
3038 .and_then(Value::as_str)?;
3039 Some(ErrorVariantIr {
3040 code: code.to_owned(),
3041 name: pascal_case(code),
3042 structured: true,
3043 payload: object
3044 .get("properties")
3045 .and_then(Value::as_object)
3046 .and_then(|properties| properties.get("payload"))
3047 .map(type_ir_from_schema),
3048 payload_required: required_fields(variant).contains("payload"),
3049 })
3050 })
3051 .collect::<Vec<_>>();
3052 variants.sort_by(|left, right| left.code.cmp(&right.code));
3053 variants
3054}
3055
3056fn error_variant_values(schema: &Value) -> BTreeMap<String, Value> {
3057 let variants = schema
3058 .get("oneOf")
3059 .and_then(Value::as_array)
3060 .into_iter()
3061 .flatten();
3062 variants
3063 .filter_map(|variant| {
3064 let code = if let Some(code) = variant.get("const").and_then(Value::as_str) {
3065 code.to_owned()
3066 } else {
3067 variant
3068 .get("properties")
3069 .and_then(Value::as_object)
3070 .and_then(|properties| properties.get("code"))
3071 .and_then(|code| code.get("const"))
3072 .and_then(Value::as_str)
3073 .map(ToOwned::to_owned)?
3074 };
3075 Some((code, variant.clone()))
3076 })
3077 .collect()
3078}
3079
3080fn required_fields(schema: &Value) -> BTreeSet<String> {
3081 schema
3082 .get("required")
3083 .and_then(Value::as_array)
3084 .into_iter()
3085 .flatten()
3086 .filter_map(Value::as_str)
3087 .map(ToOwned::to_owned)
3088 .collect()
3089}
3090
3091fn is_nullable(schema: &Value) -> bool {
3092 schema
3093 .get("type")
3094 .and_then(Value::as_array)
3095 .is_some_and(|types| types.iter().any(|value| value == "null"))
3096 || schema
3097 .get("anyOf")
3098 .and_then(Value::as_array)
3099 .is_some_and(|schemas| schemas.iter().any(is_null_schema))
3100 || schema
3101 .get("oneOf")
3102 .and_then(Value::as_array)
3103 .is_some_and(|schemas| schemas.iter().any(is_null_schema))
3104}
3105
3106fn is_null_schema(schema: &Value) -> bool {
3107 schema.get("type").is_some_and(|value| value == "null")
3108 || schema.get("const").is_some_and(Value::is_null)
3109}
3110
3111fn schema_type_name(schema: &Value) -> Option<&str> {
3112 schema.get("type").and_then(|value| match value {
3113 Value::String(value) => Some(value.as_str()),
3114 Value::Array(values) => values
3115 .iter()
3116 .find_map(Value::as_str)
3117 .filter(|value| *value != "null"),
3118 _ => None,
3119 })
3120}
3121
3122fn canonical_json(value: &Value) -> String {
3123 serde_json::to_string(value).expect("JSON values are serializable")
3124}
3125
3126fn schema_constraints_signature(schema: &Value) -> String {
3127 let mut schema = schema.as_object().cloned().unwrap_or_default();
3128 for key in [
3129 "type",
3130 "properties",
3131 "required",
3132 "additionalProperties",
3133 "items",
3134 ] {
3135 schema.remove(key);
3136 }
3137 canonical_json(&Value::Object(schema))
3138}
3139
3140fn is_identifier(value: &str) -> bool {
3141 let mut characters = value.chars();
3142 characters
3143 .next()
3144 .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
3145 && characters
3146 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
3147}
3148
3149fn is_rust_member_name(value: &str) -> bool {
3150 !matches!(snake_case(value).as_str(), "self" | "super" | "crate")
3151}
3152
3153fn is_rust_type_identifier(value: &str) -> bool {
3154 let mut characters = value.chars();
3155 let valid = characters
3156 .next()
3157 .is_some_and(|character| character.is_ascii_alphabetic() || character == '_')
3158 && characters.all(|character| character.is_ascii_alphanumeric() || character == '_');
3159 valid && value != "Self"
3160}
3161
3162fn is_generated_type_name(value: &str) -> bool {
3163 is_rust_type_identifier(value)
3164 && !matches!(
3165 value,
3166 "Self"
3167 | "String"
3168 | "Int64"
3169 | "Uint64"
3170 | "Bytes"
3171 | "Timestamp"
3172 | "Duration"
3173 | "OptionalValue"
3174 | "UnknownDomainError"
3175 | "InvocationContext"
3176 | "PluginDependencies"
3177 | "NativeRequestEndpoint"
3178 | "NativeRequestHandle"
3179 | "RequestCapability"
3180 | "RuntimeFailure"
3181 | "LocalBoxFuture"
3182 )
3183}
3184
3185fn is_generated_enum_variant(value: &str) -> bool {
3186 is_rust_type_identifier(value)
3187}
3188
3189fn pascal_case(value: &str) -> String {
3190 let mut output = String::new();
3191 for part in value.split(|character: char| !character.is_ascii_alphanumeric()) {
3192 if part.is_empty() || part.chars().all(char::is_numeric) {
3193 continue;
3194 }
3195 let mut chars = part.chars();
3196 if let Some(first) = chars.next() {
3197 output.extend(first.to_uppercase());
3198 output.push_str(chars.as_str());
3199 }
3200 }
3201 if output.is_empty() {
3202 "Value".to_owned()
3203 } else if output
3204 .chars()
3205 .next()
3206 .is_some_and(|character| character.is_ascii_digit())
3207 {
3208 format!("Value{output}")
3209 } else {
3210 output
3211 }
3212}
3213
3214fn snake_case(value: &str) -> String {
3215 let mut output = String::new();
3216 for (index, character) in value.chars().enumerate() {
3217 if character.is_ascii_alphanumeric() {
3218 if character.is_ascii_uppercase() && index != 0 {
3219 output.push('_');
3220 }
3221 output.push(character.to_ascii_lowercase());
3222 } else if !output.ends_with('_') {
3223 output.push('_');
3224 }
3225 }
3226 let output = output.trim_matches('_').to_owned();
3227 if output.is_empty() {
3228 "value".to_owned()
3229 } else {
3230 output
3231 }
3232}
3233
3234fn screaming_snake_case(value: &str) -> String {
3235 snake_case(value).to_ascii_uppercase()
3236}
3237
3238fn rust_field_name(value: &str) -> String {
3239 let name = snake_case(value);
3240 if matches!(name.as_str(), "self" | "super" | "crate") {
3241 return format!("{name}_");
3242 }
3243 if matches!(
3244 name.as_str(),
3245 "as" | "break"
3246 | "const"
3247 | "continue"
3248 | "crate"
3249 | "else"
3250 | "enum"
3251 | "extern"
3252 | "false"
3253 | "fn"
3254 | "for"
3255 | "if"
3256 | "impl"
3257 | "in"
3258 | "let"
3259 | "loop"
3260 | "match"
3261 | "mod"
3262 | "move"
3263 | "mut"
3264 | "pub"
3265 | "ref"
3266 | "return"
3267 | "self"
3268 | "Self"
3269 | "static"
3270 | "struct"
3271 | "super"
3272 | "trait"
3273 | "true"
3274 | "type"
3275 | "unsafe"
3276 | "use"
3277 | "where"
3278 | "while"
3279 | "async"
3280 | "await"
3281 | "dyn"
3282 ) {
3283 format!("r#{name}")
3284 } else {
3285 name
3286 }
3287}
3288
3289fn quote_string(value: &str) -> String {
3290 serde_json::to_string(value).expect("string is serializable")
3291}
3292
3293fn typescript_property_name(value: &str) -> String {
3294 let mut characters = value.chars();
3295 let valid_identifier = characters
3296 .next()
3297 .is_some_and(|character| character.is_ascii_alphabetic() || matches!(character, '_' | '$'))
3298 && characters
3299 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$'));
3300 if valid_identifier {
3301 value.to_owned()
3302 } else {
3303 quote_string(value)
3304 }
3305}
3306
3307struct RustTypes {
3308 declarations: Vec<String>,
3309 declared: BTreeSet<String>,
3310}
3311
3312impl RustTypes {
3313 fn new() -> Self {
3314 Self {
3315 declarations: Vec::new(),
3316 declared: BTreeSet::new(),
3317 }
3318 }
3319
3320 fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3321 if !self.declared.insert(name.to_owned()) {
3322 return name.to_owned();
3323 }
3324 let placeholder = self.declarations.len();
3325 self.declarations.push(String::new());
3326 let mut rendered_fields = Vec::new();
3327 for field in fields {
3328 let type_name = self.type_for_non_null(
3329 field.ty.non_null(),
3330 &format!("{name}{}", pascal_case(&field.name)),
3331 );
3332 let field_type = if field.required {
3333 if field.ty.is_nullable() {
3334 format!("Option<{type_name}>")
3335 } else {
3336 type_name
3337 }
3338 } else if field.ty.is_nullable() {
3339 format!("OptionalValue<{type_name}>")
3340 } else {
3341 format!("Option<{type_name}>")
3342 };
3343 let mut attributes = vec![format!(
3344 " #[serde(rename = {})]",
3345 quote_string(&field.name)
3346 )];
3347 if !field.required {
3348 if field.ty.is_nullable() {
3349 attributes.push(" #[serde(default)]".to_owned());
3350 attributes
3351 .push(" #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
3352 attributes.push(
3353 " #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_optional_value\")]"
3354 .to_owned(),
3355 );
3356 } else {
3357 attributes
3358 .push(" #[serde(skip_serializing_if = \"Option::is_none\")]".to_owned());
3359 }
3360 }
3361 if field.required {
3362 attributes
3363 .push(" #[serde(deserialize_with = \"lenso_contract_runtime::serde::deserialize_required\")]".to_owned());
3364 }
3365 rendered_fields.push(format!(
3366 "{}\n pub {}: {field_type},",
3367 attributes.join("\n"),
3368 rust_field_name(&field.name)
3369 ));
3370 }
3371 let debug_impl = rust_debug::render(
3372 name,
3373 fields.iter().map(|field| {
3374 (
3375 field.name.as_str(),
3376 rust_field_name(&field.name),
3377 field.sensitive,
3378 )
3379 }),
3380 );
3381 let derives = if debug_impl.is_some() {
3382 "Clone, PartialEq, serde::Serialize, serde::Deserialize"
3383 } else {
3384 "Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize"
3385 };
3386 self.declarations[placeholder] = format!(
3387 "#[derive({derives})]\npub struct {name} {{\n{}\n}}\n{debug_impl}",
3388 rendered_fields.join("\n"),
3389 debug_impl = debug_impl.unwrap_or_default(),
3390 );
3391 name.to_owned()
3392 }
3393
3394 fn enum_type(&mut self, name: &str, values: &[String]) -> String {
3395 if !self.declared.insert(name.to_owned()) {
3396 return name.to_owned();
3397 }
3398 let variants = values
3399 .iter()
3400 .map(|value| {
3401 format!(
3402 " #[serde(rename = {})]\n {},",
3403 quote_string(value),
3404 pascal_case(value)
3405 )
3406 })
3407 .collect::<Vec<_>>();
3408 self.declarations.push(format!(
3409 "#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum {name} {{\n{}\n}}\n",
3410 variants.join("\n")
3411 ));
3412 name.to_owned()
3413 }
3414
3415 fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3416 let base = self.type_for_non_null(ty.non_null(), nested_name);
3417 if ty.is_nullable() {
3418 format!("Option<{base}>")
3419 } else {
3420 base
3421 }
3422 }
3423
3424 fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3425 match ty {
3426 TypeIr::Any => "serde_json::Value".to_owned(),
3427 TypeIr::String => "String".to_owned(),
3428 TypeIr::RawJson => "RawJson".to_owned(),
3429 TypeIr::Enum { name, values } => {
3430 self.enum_type(name.as_deref().unwrap_or(nested_name), values)
3431 }
3432 TypeIr::Int64 => "Int64".to_owned(),
3433 TypeIr::Uint64 => "Uint64".to_owned(),
3434 TypeIr::Bytes => "Bytes".to_owned(),
3435 TypeIr::Timestamp => "Timestamp".to_owned(),
3436 TypeIr::Duration => "Duration".to_owned(),
3437 TypeIr::Integer => "i64".to_owned(),
3438 TypeIr::Number => "f64".to_owned(),
3439 TypeIr::Boolean => "bool".to_owned(),
3440 TypeIr::Null => "()".to_owned(),
3441 TypeIr::Array(items) => {
3442 format!(
3443 "Vec<{}>",
3444 self.type_for(items, &format!("{nested_name}Item"))
3445 )
3446 }
3447 TypeIr::Object {
3448 name,
3449 fields,
3450 additional,
3451 } => {
3452 let nested_name = name.as_deref().unwrap_or(nested_name);
3453 if fields.is_empty() {
3454 match additional {
3455 ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3456 ObjectAdditionalIr::Any => {
3457 "std::collections::BTreeMap<String, serde_json::Value>".to_owned()
3458 }
3459 ObjectAdditionalIr::Typed(values) => format!(
3460 "std::collections::BTreeMap<String, {}>",
3461 self.type_for(values, &format!("{nested_name}Value"))
3462 ),
3463 }
3464 } else {
3465 self.object(nested_name, fields)
3466 }
3467 }
3468 TypeIr::Nullable(inner) => {
3469 format!("Option<{}>", self.type_for_non_null(inner, nested_name))
3470 }
3471 }
3472 }
3473}
3474
3475struct TypeScriptTypes {
3476 declarations: Vec<String>,
3477 declared: BTreeSet<String>,
3478}
3479
3480impl TypeScriptTypes {
3481 fn new() -> Self {
3482 Self {
3483 declarations: Vec::new(),
3484 declared: BTreeSet::new(),
3485 }
3486 }
3487
3488 fn object(&mut self, name: &str, fields: &[FieldIr]) -> String {
3489 if !self.declared.insert(name.to_owned()) {
3490 return name.to_owned();
3491 }
3492 let placeholder = self.declarations.len();
3493 self.declarations.push(String::new());
3494 let mut rendered_fields = Vec::new();
3495 for field in fields {
3496 let type_name = self.type_for_non_null(
3497 field.ty.non_null(),
3498 &format!("{name}{}", pascal_case(&field.name)),
3499 );
3500 let optional = if field.required { "" } else { "?" };
3501 let field_type = if field.ty.is_nullable() {
3502 format!("{type_name} | null")
3503 } else {
3504 type_name
3505 };
3506 rendered_fields.push(format!(
3507 " {}{optional}: {field_type};",
3508 typescript_property_name(&field.name)
3509 ));
3510 }
3511 self.declarations[placeholder] = format!(
3512 "export interface {name} {{\n{}\n}}\n",
3513 rendered_fields.join("\n")
3514 );
3515 name.to_owned()
3516 }
3517
3518 fn type_for(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3519 let base = self.type_for_non_null(ty.non_null(), nested_name);
3520 if ty.is_nullable() {
3521 format!("{base} | null")
3522 } else {
3523 base
3524 }
3525 }
3526
3527 fn type_for_non_null(&mut self, ty: &TypeIr, nested_name: &str) -> String {
3528 match ty {
3529 TypeIr::Any => "unknown".to_owned(),
3530 TypeIr::String | TypeIr::RawJson => "string".to_owned(),
3531 TypeIr::Int64 => "Int64".to_owned(),
3532 TypeIr::Uint64 => "Uint64".to_owned(),
3533 TypeIr::Bytes => "Bytes".to_owned(),
3534 TypeIr::Timestamp => "Timestamp".to_owned(),
3535 TypeIr::Duration => "Duration".to_owned(),
3536 TypeIr::Integer | TypeIr::Number => "number".to_owned(),
3537 TypeIr::Boolean => "boolean".to_owned(),
3538 TypeIr::Null => "null".to_owned(),
3539 TypeIr::Enum { values, .. } => values
3540 .iter()
3541 .map(|value| quote_string(value))
3542 .collect::<Vec<_>>()
3543 .join(" | "),
3544 TypeIr::Array(items) => {
3545 format!(
3546 "Array<{}>",
3547 self.type_for(items, &format!("{nested_name}Item"))
3548 )
3549 }
3550 TypeIr::Object {
3551 name,
3552 fields,
3553 additional,
3554 } => {
3555 let nested_name = name.as_deref().unwrap_or(nested_name);
3556 if fields.is_empty() {
3557 match additional {
3558 ObjectAdditionalIr::Closed => self.object(nested_name, fields),
3559 ObjectAdditionalIr::Any => "Record<string, unknown>".to_owned(),
3560 ObjectAdditionalIr::Typed(values) => format!(
3561 "Record<string, {}>",
3562 self.type_for(values, &format!("{nested_name}Value"))
3563 ),
3564 }
3565 } else {
3566 self.object(nested_name, fields)
3567 }
3568 }
3569 TypeIr::Nullable(inner) => {
3570 format!("{} | null", self.type_for_non_null(inner, nested_name))
3571 }
3572 }
3573 }
3574}
3575
3576#[allow(clippy::too_many_lines)]
3577fn generate_rust(contract: &ContractIr) -> String {
3578 let capability_name = pascal_case(
3579 contract
3580 .capability_id
3581 .split('@')
3582 .next()
3583 .and_then(|identity| identity.rsplit('.').next())
3584 .unwrap_or("Capability"),
3585 );
3586 let capability_const = screaming_snake_case(&capability_name);
3587 let native_support_name = format!("__LensoNativeSupport{capability_name}");
3588 let mut types = RustTypes::new();
3589 let mut operation_rows = Vec::new();
3590 let mut stream_operation_rows = Vec::new();
3591 let mut event_operation_rows = Vec::new();
3592 let mut operation_markers = Vec::new();
3593 let mut provider_methods = Vec::new();
3594 let mut provider_lowering_methods = Vec::new();
3595 let mut provider_result_conversions = Vec::new();
3596 let mut endpoint_arms = Vec::new();
3597 let mut stream_endpoint_arms = Vec::new();
3598 let mut event_endpoint_arms = Vec::new();
3599 let mut client_fields = Vec::new();
3600 let mut client_initializers = Vec::new();
3601 let mut many_client_initializers = Vec::new();
3602 let mut client_methods = Vec::new();
3603 let mut invocation_errors = Vec::new();
3604 let mut error_codecs = Vec::new();
3605 let mut wire_codecs = Vec::new();
3606
3607 for operation in &contract.operations {
3608 let operation_name = pascal_case(&operation.name);
3609 let request_name = format!("{operation_name}Request");
3610 let response_name = format!("{operation_name}Response");
3611 let error_name = format!("{operation_name}Error");
3612 let marker_name = if contract.operations.len() == 1 {
3613 capability_name.clone()
3614 } else {
3615 format!("{capability_name}{operation_name}")
3616 };
3617 let request_type = types.type_for(&operation.request, &request_name);
3618 let response_type = types.type_for(&operation.response, &response_name);
3619 let known_errors = &operation.domain_errors;
3620 let error_definition = if known_errors.is_empty() {
3621 format!(
3622 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n Unknown(UnknownDomainError),\n}}\n"
3623 )
3624 } else {
3625 let variants = known_errors
3626 .iter()
3627 .map(|variant| {
3628 if let Some(payload) = &variant.payload {
3629 let payload_name = format!("{error_name}{}Payload", variant.name);
3630 let payload_type =
3631 types.type_for_non_null(payload.non_null(), &payload_name);
3632 let payload_type = if variant.payload_required {
3633 if payload.is_nullable() {
3634 format!("Option<{payload_type}>")
3635 } else {
3636 payload_type
3637 }
3638 } else if payload.is_nullable() {
3639 format!("OptionalValue<{payload_type}>")
3640 } else {
3641 format!("Option<{payload_type}>")
3642 };
3643 format!(" {} {{ payload: {payload_type} }},", variant.name)
3644 } else {
3645 format!(" {},", variant.name)
3646 }
3647 })
3648 .chain(std::iter::once(
3649 " Unknown(UnknownDomainError),".to_owned(),
3650 ))
3651 .collect::<Vec<_>>();
3652 format!(
3653 "#[derive(Clone, Debug, PartialEq)]\npub enum {error_name} {{\n{}\n}}\n",
3654 variants.join("\n")
3655 )
3656 };
3657 types.declarations.push(error_definition);
3658 error_codecs.push(generate_rust_error_codec(&error_name, known_errors));
3659 wire_codecs.push(generate_rust_wire_codecs(
3660 &operation.name,
3661 &request_type,
3662 &response_type,
3663 &error_name,
3664 ));
3665 if operation.interaction == "event" {
3666 wire_codecs.push(generate_rust_event_codecs(&operation.name, &request_type));
3667 }
3668 let operation_const = screaming_snake_case(&operation.name);
3669 operation_markers.push(match operation.interaction.as_str() {
3670 "request" => {
3671 let provider_method = rust_field_name(&operation.name);
3672 format!(
3673 "#[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"
3674 )
3675 }
3676 "stream" => format!(
3677 "#[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"
3678 ),
3679 "event" => format!(
3680 "#[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"
3681 ),
3682 _ => unreachable!("Descriptor validation restricts interactions"),
3683 });
3684 if operation.interaction == "request" {
3685 let invocation_error_name = if contract.operations.len() == 1 {
3686 format!("{capability_name}InvocationError")
3687 } else {
3688 format!("{capability_name}{operation_name}InvocationError")
3689 };
3690 operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
3691 provider_methods.push(format!(
3692 " fn {}(&self, context: InvocationContext, request: {request_type}) -> NativeRequestFuture<{marker_name}>;",
3693 rust_field_name(&operation.name),
3694 ));
3695 let field = rust_field_name(&operation.name);
3696 let conversion = format!("__LensoInto{capability_name}{operation_name}Result");
3697 provider_result_conversions.push(format!(
3698 "#[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"
3699 ));
3700 provider_lowering_methods.push(format!(
3701 " 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 }}"
3702 ));
3703 endpoint_arms.push(format!(
3704 " {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 }}",
3705 rust_field_name(&operation.name),
3706 ));
3707 let field = rust_field_name(&operation.name);
3708 client_fields.push(format!(" {field}: NativeRequestHandle<{marker_name}>,"));
3709 client_initializers.push(format!(
3710 " {field}: dependencies.one::<{marker_name}>()?,"
3711 ));
3712 many_client_initializers.push(format!(
3713 " {field}: binding.handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3714 ));
3715 client_methods.push(format!(
3716 " 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 }}",
3717 screaming_snake_case(&operation.name),
3718 screaming_snake_case(&operation.name),
3719 ));
3720 invocation_errors.push(format!(
3721 "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n Domain({error_name}),\n Runtime(RuntimeFailure),\n}}\n"
3722 ));
3723 } else if operation.interaction == "stream" {
3724 let invocation_error_name = if contract.operations.len() == 1 {
3725 format!("{capability_name}InvocationError")
3726 } else {
3727 format!("{capability_name}{operation_name}InvocationError")
3728 };
3729 stream_operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
3730 provider_methods.push(format!(
3731 " fn {}(&self, context: InvocationContext, request: {request_type}) -> LocalBoxFuture<'static, Result<Box<dyn NativeStreamSession>, {invocation_error_name}>>;",
3732 rust_field_name(&operation.name)
3733 ));
3734 let field = rust_field_name(&operation.name);
3735 let conversion = format!("__LensoInto{capability_name}{operation_name}StreamResult");
3736 provider_result_conversions.push(format!(
3737 "#[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"
3738 ));
3739 provider_lowering_methods.push(format!(
3740 " 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 }}"
3741 ));
3742 stream_endpoint_arms.push(format!(
3743 " {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 }}",
3744 rust_field_name(&operation.name),
3745 ));
3746 let field = rust_field_name(&operation.name);
3747 client_fields.push(format!(" {field}: NativeStreamHandle<{marker_name}>,"));
3748 client_initializers.push(format!(
3749 " {field}: dependencies.one_stream::<{marker_name}>()?,"
3750 ));
3751 many_client_initializers.push(format!(
3752 " {field}: binding.stream_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3753 ));
3754 client_methods.push(format!(
3755 " 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 }}"
3756 ));
3757 invocation_errors.push(format!(
3758 "#[derive(Clone, Debug, PartialEq)]\npub enum {invocation_error_name} {{\n Domain({error_name}),\n Runtime(RuntimeFailure),\n}}\n"
3759 ));
3760 } else {
3761 event_operation_rows.push(format!(" {operation_const}_OPERATION,\n"));
3762 provider_methods.push(format!(
3763 " fn {}(&self, context: InvocationContext, event: {request_type}) -> LocalBoxFuture<'static, Result<(), RuntimeFailure>>;",
3764 rust_field_name(&operation.name)
3765 ));
3766 let field = rust_field_name(&operation.name);
3767 let conversion = format!("__LensoInto{capability_name}{operation_name}EventResult");
3768 provider_result_conversions.push(format!(
3769 "#[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"
3770 ));
3771 provider_lowering_methods.push(format!(
3772 " 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 }}"
3773 ));
3774 event_endpoint_arms.push(format!(
3775 " {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 }}",
3776 rust_field_name(&operation.name),
3777 ));
3778 let field = rust_field_name(&operation.name);
3779 client_fields.push(format!(" {field}: NativeEventHandle<{marker_name}>,"));
3780 client_initializers.push(format!(
3781 " {field}: dependencies.many_event::<{marker_name}>()?,"
3782 ));
3783 many_client_initializers.push(format!(
3784 " {field}: binding.event_handle().ok_or(RuntimeFailure::Unavailable {{ capability: CAPABILITY_ID }})?.typed::<{marker_name}>()?,"
3785 ));
3786 client_methods.push(format!(
3787 " 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 }}"
3788 ));
3789 }
3790 }
3791
3792 let request_endpoint_impl = if operation_rows.is_empty() {
3793 String::new()
3794 } else {
3795 format!(
3796 "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",
3797 operation_rows.concat(),
3798 endpoint_arms.join(",\n")
3799 )
3800 };
3801 let stream_endpoint_impl = if stream_operation_rows.is_empty() {
3802 String::new()
3803 } else {
3804 format!(
3805 "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",
3806 stream_operation_rows.concat(),
3807 stream_endpoint_arms.join(",\n")
3808 )
3809 };
3810 let event_endpoint_impl = if event_operation_rows.is_empty() {
3811 String::new()
3812 } else {
3813 format!(
3814 "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",
3815 event_operation_rows.concat(),
3816 event_endpoint_arms.join(",\n")
3817 )
3818 };
3819 let mut output = String::new();
3820 output.push_str(GENERATED_HEADER);
3821 let has_request_operations = !operation_rows.is_empty();
3822 let has_stream_operations = !stream_operation_rows.is_empty();
3823 let has_event_operations = !event_operation_rows.is_empty();
3824 let mut kernel_imports = vec!["InvocationContext", "PluginDependencies", "RuntimeFailure"];
3825 if has_request_operations {
3826 kernel_imports.extend([
3827 "NativeRequestFuture",
3828 "NativeRequestEndpoint",
3829 "NativeRequestHandle",
3830 "RequestCapability",
3831 ]);
3832 }
3833 if has_stream_operations {
3834 kernel_imports.extend([
3835 "NativeStream",
3836 "NativeStreamEndpoint",
3837 "NativeStreamHandle",
3838 "NativeStreamSession",
3839 "StreamCapability",
3840 "StreamEvent",
3841 ]);
3842 }
3843 if has_event_operations {
3844 kernel_imports.extend([
3845 "EventCapability",
3846 "EventPublishResult",
3847 "NativeEventEndpoint",
3848 "NativeEventHandle",
3849 ]);
3850 }
3851 kernel_imports.sort_unstable();
3852 writeln!(
3853 output,
3854 "use std::{{fmt, rc::Rc}};\nuse futures::future::LocalBoxFuture;\nuse lenso_kernel::{{{}}};\n",
3855 kernel_imports.join(", ")
3856 )
3857 .expect("writing to a String cannot fail");
3858 output.push_str(
3859 "use lenso_plugin_authoring::{BoundCapabilityClient, CapabilityClient, CapabilityClientMany};\n",
3860 );
3861 writeln!(
3862 output,
3863 "pub const CAPABILITY_ID: &str = {};",
3864 quote_string(&contract.capability_id)
3865 )
3866 .expect("writing to a String cannot fail");
3867 writeln!(
3868 output,
3869 "pub const DESCRIPTOR_VERSION: &str = {};",
3870 quote_string(&contract.version)
3871 )
3872 .expect("writing to a String cannot fail");
3873 writeln!(output, "pub const PORTABLE: bool = {};", contract.portable)
3874 .expect("writing to a String cannot fail");
3875 writeln!(
3876 output,
3877 "pub const CROSS_LANE_TRANSFER: bool = {};",
3878 contract.cross_lane_transfer
3879 )
3880 .expect("writing to a String cannot fail");
3881 writeln!(
3882 output,
3883 "pub const {capability_const}_CAPABILITY_ID: &str = CAPABILITY_ID;"
3884 )
3885 .expect("writing to a String cannot fail");
3886 write!(
3887 output,
3888 "pub const {capability_const}_DESCRIPTOR_VERSION: &str = DESCRIPTOR_VERSION;\n\n"
3889 )
3890 .expect("writing to a String cannot fail");
3891
3892 let capability_macro_name = snake_case(&capability_name);
3893 let client_macro_name = snake_case(&format!("{capability_name}Client"));
3894 let operations = contract
3895 .operations
3896 .iter()
3897 .map(|operation| Value::String(operation.name.clone()))
3898 .collect::<Vec<_>>();
3899 let operation_kinds = contract
3900 .operations
3901 .iter()
3902 .filter(|operation| operation.interaction != "request")
3903 .map(|operation| {
3904 (
3905 operation.name.clone(),
3906 Value::String(operation.interaction.clone()),
3907 )
3908 })
3909 .collect::<Map<_, _>>();
3910 let provided_fragment = canonical_json(&serde_json::json!({
3911 "capability_id": contract.capability_id,
3912 "descriptor_version": contract.version,
3913 "operations": operations,
3914 "operation_kinds": operation_kinds,
3915 "default_admission": {
3916 "queue_capacity": 0,
3917 "max_concurrency": 1
3918 },
3919 "operation_admissions": {},
3920 "event_admission": null,
3921 "cross_lane_transfer": contract.cross_lane_transfer
3922 }));
3923 let required_fragment = canonical_json(&serde_json::json!({
3924 "capability_id": contract.capability_id,
3925 "descriptor_version": contract.version,
3926 "cardinality": "one"
3927 }));
3928 let required_many_fragment = canonical_json(&serde_json::json!({
3929 "capability_id": contract.capability_id,
3930 "descriptor_version": contract.version,
3931 "cardinality": "many"
3932 }));
3933 writeln!(
3934 output,
3935 "#[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",
3936 quote_string(&provided_fragment),
3937 quote_string(&required_fragment),
3938 quote_string(&required_many_fragment),
3939 )
3940 .expect("writing to a String cannot fail");
3941 for operation in &contract.operations {
3942 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
3943 writeln!(
3944 output,
3945 "pub const {operation_const}: &str = {};",
3946 quote_string(&operation.name)
3947 )
3948 .expect("writing to a String cannot fail");
3949 }
3950 writeln!(
3951 output,
3952 "\npub use lenso_contract_runtime::{{{}}};\nuse lenso_contract_runtime::{{decode_portable_json, encode_portable_json}};\n",
3953 rust_runtime_types(contract).join(", ")
3954 )
3955 .expect("writing to a String cannot fail");
3956 for declaration in types.declarations {
3957 output.push_str(&declaration);
3958 output.push('\n');
3959 }
3960 for marker in operation_markers {
3961 output.push_str(&marker);
3962 output.push('\n');
3963 }
3964 for codec in error_codecs {
3965 output.push_str(&codec);
3966 output.push('\n');
3967 }
3968 for codec in wire_codecs {
3969 output.push_str(&codec);
3970 output.push('\n');
3971 }
3972 for conversion in provider_result_conversions {
3973 output.push_str(&conversion);
3974 output.push('\n');
3975 }
3976 write!(
3977 output,
3978 "pub trait {capability_name}Provider: fmt::Debug + 'static {{\n{}\n}}\n\n",
3979 provider_methods.join("\n")
3980 )
3981 .expect("writing to a String cannot fail");
3982 writeln!(
3983 output,
3984 "#[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",
3985 provider_lowering_methods.join("\n")
3986 )
3987 .expect("writing to a String cannot fail");
3988 if has_request_operations {
3989 write!(
3990 output,
3991 "#[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"
3992 )
3993 .expect("writing to a String cannot fail");
3994 } else {
3995 write!(
3996 output,
3997 "#[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"
3998 )
3999 .expect("writing to a String cannot fail");
4000 }
4001 output.push_str(&request_endpoint_impl);
4002 output.push_str(&stream_endpoint_impl);
4003 output.push_str(&event_endpoint_impl);
4004 let request_endpoint_value = has_request_operations.then_some(
4005 "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeRequestEndpoint>",
4006 );
4007 let stream_endpoint_value = has_stream_operations.then_some(
4008 "endpoint.clone() as ::std::rc::Rc<dyn __LensoNativeSupport::NativeStreamEndpoint>",
4009 );
4010 let event_endpoint_value = has_event_operations
4011 .then_some("endpoint as ::std::rc::Rc<dyn __LensoNativeSupport::NativeEventEndpoint>");
4012 writeln!(
4013 output,
4014 "#[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",
4015 request_endpoint_value.unwrap_or_default(),
4016 stream_endpoint_value.unwrap_or_default(),
4017 event_endpoint_value.unwrap_or_default(),
4018 )
4019 .expect("writing to a String cannot fail");
4020 let new_method = if contract.operations.len() == 1 {
4021 let field = rust_field_name(&contract.operations[0].name);
4022 let marker = &capability_name;
4023 match contract.operations[0].interaction.as_str() {
4024 "request" => format!(
4025 " pub fn new(handle: NativeRequestHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
4026 ),
4027 "stream" => format!(
4028 " pub fn new(handle: NativeStreamHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
4029 ),
4030 "event" => format!(
4031 " pub fn new(handle: NativeEventHandle<{marker}>) -> Self {{\n Self {{ {field}: handle }}\n }}\n\n"
4032 ),
4033 _ => unreachable!("Descriptor validation restricts interactions"),
4034 }
4035 } else {
4036 String::new()
4037 };
4038 write!(
4039 output,
4040 "#[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",
4041 client_fields.join("\n"),
4042 new_method,
4043 client_methods.join("\n\n"),
4044 client_initializers.join("\n"),
4045 many_client_initializers.join("\n")
4046 )
4047 .expect("writing to a String cannot fail");
4048 for error in invocation_errors {
4049 output.push_str(&error);
4050 }
4051 format!("{}\n", output.trim_end())
4052}
4053
4054fn rust_runtime_types(contract: &ContractIr) -> Vec<&'static str> {
4055 let mut types = BTreeSet::from(["UnknownDomainError"]);
4056 for operation in &contract.operations {
4057 collect_rust_runtime_types(&operation.request, &mut types);
4058 collect_rust_runtime_types(&operation.response, &mut types);
4059 for error in &operation.domain_errors {
4060 if let Some(payload) = &error.payload {
4061 if !error.payload_required && payload.is_nullable() {
4062 types.insert("OptionalValue");
4063 }
4064 collect_rust_runtime_types(payload, &mut types);
4065 }
4066 }
4067 }
4068 types.into_iter().collect()
4069}
4070
4071fn collect_rust_runtime_types(ty: &TypeIr, types: &mut BTreeSet<&'static str>) {
4072 match ty {
4073 TypeIr::Int64 => {
4074 types.insert("Int64");
4075 }
4076 TypeIr::Uint64 => {
4077 types.insert("Uint64");
4078 }
4079 TypeIr::Bytes => {
4080 types.insert("Bytes");
4081 }
4082 TypeIr::Timestamp => {
4083 types.insert("Timestamp");
4084 }
4085 TypeIr::Duration => {
4086 types.insert("Duration");
4087 }
4088 TypeIr::RawJson => {
4089 types.insert("RawJson");
4090 }
4091 TypeIr::Array(item) | TypeIr::Nullable(item) => {
4092 collect_rust_runtime_types(item, types);
4093 }
4094 TypeIr::Object {
4095 fields, additional, ..
4096 } => {
4097 for field in fields {
4098 if !field.required && field.ty.is_nullable() {
4099 types.insert("OptionalValue");
4100 }
4101 collect_rust_runtime_types(&field.ty, types);
4102 }
4103 if let ObjectAdditionalIr::Typed(value) = additional {
4104 collect_rust_runtime_types(value, types);
4105 }
4106 }
4107 TypeIr::Any
4108 | TypeIr::String
4109 | TypeIr::Integer
4110 | TypeIr::Number
4111 | TypeIr::Boolean
4112 | TypeIr::Null
4113 | TypeIr::Enum { .. } => {}
4114 }
4115}
4116
4117#[allow(clippy::too_many_lines)]
4118fn generate_rust_error_codec(error_name: &str, variants: &[ErrorVariantIr]) -> String {
4119 let mut output = String::new();
4120 writeln!(output, "impl serde::Serialize for {error_name} {{").expect("String cannot fail");
4121 output.push_str(
4122 " fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n where\n S: serde::Serializer,\n {\n",
4123 );
4124 output.push_str(" use serde::ser::SerializeMap;\n");
4125 output.push_str(" match self {\n");
4126 for variant in variants {
4127 if variant.structured {
4128 if variant.payload.is_some() {
4129 if variant.payload_required {
4130 writeln!(
4131 output,
4132 " 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 }},",
4133 variant.name,
4134 quote_string(&variant.code)
4135 )
4136 .expect("String cannot fail");
4137 } else {
4138 writeln!(
4139 output,
4140 " 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 }},",
4141 variant.name,
4142 quote_string(&variant.code)
4143 )
4144 .expect("String cannot fail");
4145 }
4146 } else {
4147 writeln!(
4148 output,
4149 " Self::{} => {{\n let mut map = serializer.serialize_map(Some(1))?;\n map.serialize_entry(\"code\", {})?;\n map.end()\n }},",
4150 variant.name,
4151 quote_string(&variant.code)
4152 )
4153 .expect("String cannot fail");
4154 }
4155 } else {
4156 writeln!(
4157 output,
4158 " Self::{} => serializer.serialize_str({}),",
4159 variant.name,
4160 quote_string(&variant.code)
4161 )
4162 .expect("String cannot fail");
4163 }
4164 }
4165 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");
4166 writeln!(
4167 output,
4168 "impl<'de> serde::Deserialize<'de> for {error_name} {{"
4169 )
4170 .expect("String cannot fail");
4171 output.push_str(
4172 " 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",
4173 );
4174 for variant in variants.iter().filter(|variant| !variant.structured) {
4175 writeln!(
4176 output,
4177 " {} => Ok(Self::{}),",
4178 quote_string(&variant.code),
4179 variant.name
4180 )
4181 .expect("String cannot fail");
4182 }
4183 output.push_str(
4184 " _ => 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",
4185 );
4186 if variants.iter().any(|variant| variant.structured) {
4187 output.push_str(" match code.as_str() {\n");
4188 for variant in variants.iter().filter(|variant| variant.structured) {
4189 if variant.payload.is_some() {
4190 if variant.payload_required {
4191 writeln!(
4192 output,
4193 " {} => {{\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 }},",
4194 quote_string(&variant.code),
4195 variant.name
4196 )
4197 .expect("String cannot fail");
4198 } else {
4199 writeln!(
4200 output,
4201 " {} => {{\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 }},",
4202 quote_string(&variant.code),
4203 variant.name
4204 )
4205 .expect("String cannot fail");
4206 }
4207 } else {
4208 writeln!(
4209 output,
4210 " {} => Ok(Self::{}),",
4211 quote_string(&variant.code),
4212 variant.name
4213 )
4214 .expect("String cannot fail");
4215 }
4216 }
4217 output.push_str(
4218 " _ => {\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",
4219 );
4220 } else {
4221 output.push_str(
4222 " 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",
4223 );
4224 }
4225 output.push_str(
4226 " other => Err(serde::de::Error::custom(format!(\"Domain Error must be a string or object, got {other}\"))),\n }\n }\n}\n",
4227 );
4228 output
4229}
4230
4231#[allow(clippy::too_many_lines)]
4232fn generate_rust_runtime(contract: &ContractIr) -> Result<String, CodegenError> {
4233 let capability_name = pascal_case(
4234 contract
4235 .capability_id
4236 .split('@')
4237 .next()
4238 .and_then(|identity| identity.rsplit('.').next())
4239 .unwrap_or("Capability"),
4240 );
4241 let codec_name = format!("{capability_name}JsonCodec");
4242 let guest_client_name = format!("{capability_name}GuestClient");
4243 let mut types = RustTypes::new();
4244 let mut request_operations = Vec::new();
4245 let mut stream_operations = Vec::new();
4246 let mut encode_arms = Vec::new();
4247 let mut response_arms = Vec::new();
4248 let mut error_arms = Vec::new();
4249 let mut host_request_arms = Vec::new();
4250 let mut stream_open_arms = Vec::new();
4251 let mut stream_encode_arms = Vec::new();
4252 let mut stream_decode_arms = Vec::new();
4253 let mut stream_error_arms = Vec::new();
4254 let mut host_stream_arms = Vec::new();
4255 let mut guest_methods = Vec::new();
4256
4257 for operation in &contract.operations {
4258 if operation.interaction == "event" {
4259 return Err(CodegenError::UnsupportedInteraction {
4260 operation: operation.name.clone(),
4261 interaction: operation.interaction.clone(),
4262 });
4263 }
4264 let operation_name = pascal_case(&operation.name);
4265 let marker_name = if contract.operations.len() == 1 {
4266 capability_name.clone()
4267 } else {
4268 format!("{capability_name}{operation_name}")
4269 };
4270 let request_type = types.type_for(&operation.request, &format!("{operation_name}Request"));
4271 let response_type =
4272 types.type_for(&operation.response, &format!("{operation_name}Response"));
4273 let error_type = format!("{operation_name}Error");
4274 let operation_const = format!("{}_OPERATION", screaming_snake_case(&operation.name));
4275 let method_name = rust_field_name(&operation.name);
4276 match operation.interaction.as_str() {
4277 "request" => {
4278 request_operations.push(operation_const.clone());
4279 encode_arms.push(format!(
4280 " {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 }}"
4281 ));
4282 response_arms.push(format!(
4283 " {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()),"
4284 ));
4285 error_arms.push(format!(
4286 " {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()),"
4287 ));
4288 host_request_arms.push(format!(
4289 " {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 }}"
4290 ));
4291 guest_methods.push(format!(
4292 " 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 }}"
4293 ));
4294 }
4295 "stream" => {
4296 stream_operations.push(operation_const.clone());
4297 stream_open_arms.push(format!(
4298 " {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 }}"
4299 ));
4300 stream_encode_arms.push(format!(
4301 " {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 }}"
4302 ));
4303 stream_decode_arms.push(format!(
4304 " {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()),"
4305 ));
4306 stream_error_arms.push(format!(
4307 " {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()),"
4308 ));
4309 host_stream_arms.push(format!(
4310 " {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 }}"
4311 ));
4312 guest_methods.push(format!(
4313 " 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 }}"
4314 ));
4315 }
4316 _ => unreachable!("Descriptor validation restricts interactions"),
4317 }
4318 }
4319
4320 let encode_dispatch = runtime_codec_dispatch(&encode_arms);
4321 let response_dispatch = runtime_codec_dispatch(&response_arms);
4322 let error_dispatch = runtime_codec_dispatch(&error_arms);
4323 let host_request_dispatch = runtime_codec_host_dispatch(&host_request_arms);
4324 let stream_open_dispatch = runtime_codec_dispatch(&stream_open_arms);
4325 let stream_encode_dispatch = runtime_codec_dispatch(&stream_encode_arms);
4326 let stream_decode_dispatch = runtime_codec_dispatch(&stream_decode_arms);
4327 let stream_error_dispatch = runtime_codec_dispatch(&stream_error_arms);
4328 let host_stream_dispatch = runtime_codec_host_dispatch(&host_stream_arms);
4329 let request_parameter = if request_operations.is_empty() {
4330 "_request"
4331 } else {
4332 "request"
4333 };
4334 let request_value_parameter = if request_operations.is_empty() {
4335 "_value"
4336 } else {
4337 "value"
4338 };
4339 let stream_request_parameter = if stream_operations.is_empty() {
4340 "_request"
4341 } else {
4342 "request"
4343 };
4344 let stream_message_parameter = if stream_operations.is_empty() {
4345 "_message"
4346 } else {
4347 "message"
4348 };
4349 let stream_value_parameter = if stream_operations.is_empty() {
4350 "_value"
4351 } else {
4352 "value"
4353 };
4354 let (host_request_dependency_parameter, host_request_parameter, host_request_context_parameter) =
4355 if request_operations.is_empty() {
4356 ("_dependency", "_request", "_context")
4357 } else {
4358 ("dependency", "request", "context")
4359 };
4360 let (host_stream_dependency_parameter, host_stream_parameter, host_stream_context_parameter) =
4361 if stream_operations.is_empty() {
4362 ("_dependency", "_request", "_context")
4363 } else {
4364 ("dependency", "request", "context")
4365 };
4366
4367 let mut output = generate_rust(contract);
4368 write!(
4369 output,
4370 "\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",
4371 request_operations.join(", "),
4372 stream_operations.join(", "),
4373 guest_methods.join("\n\n"),
4374 )
4375 .expect("writing generated Rust to a String cannot fail");
4376 write!(
4377 output,
4378 "\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",
4379 request_operations.join(", "),
4380 stream_operations.join(", "),
4381 )
4382 .expect("writing generated Rust to a String cannot fail");
4383 Ok(output)
4384}
4385
4386fn runtime_codec_host_dispatch(arms: &[String]) -> String {
4387 if arms.is_empty() {
4388 " Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation))))"
4389 .to_owned()
4390 } else {
4391 let arms = arms
4392 .iter()
4393 .map(|arm| arm.trim_end_matches(','))
4394 .collect::<Vec<_>>()
4395 .join(",\n");
4396 format!(
4397 " match operation.as_str() {{\n{arms},\n _ => Box::pin(std::future::ready(Err(runtime_codec_unknown_operation(&operation)))),\n }}"
4398 )
4399 }
4400}
4401
4402fn runtime_codec_dispatch(arms: &[String]) -> String {
4403 if arms.is_empty() {
4404 " Err(runtime_codec_unknown_operation(operation))".to_owned()
4405 } else {
4406 let arms = arms
4407 .iter()
4408 .map(|arm| arm.trim_end_matches(','))
4409 .collect::<Vec<_>>()
4410 .join(",\n");
4411 format!(
4412 " match operation {{\n{arms},\n _ => Err(runtime_codec_unknown_operation(operation)),\n }}"
4413 )
4414 }
4415}
4416
4417fn generate_rust_wire_codecs(
4418 operation: &str,
4419 request_type: &str,
4420 response_type: &str,
4421 error_type: &str,
4422) -> String {
4423 let stem = snake_case(operation);
4424 format!(
4425 "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"
4426 )
4427}
4428
4429fn generate_rust_event_codecs(operation: &str, event_type: &str) -> String {
4430 let stem = snake_case(operation);
4431 format!(
4432 "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"
4433 )
4434}
4435
4436fn generate_typescript_codecs(
4437 operation: &str,
4438 request_type: &str,
4439 response_type: &str,
4440 error_type: &str,
4441 variants: &[ErrorVariantIr],
4442) -> String {
4443 let stem = pascal_case(operation);
4444 let known_strings = variants
4445 .iter()
4446 .filter(|variant| !variant.structured)
4447 .map(|variant| quote_string(&variant.code))
4448 .collect::<Vec<_>>()
4449 .join(", ");
4450 format!(
4451 "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"
4452 )
4453}
4454
4455fn generate_typescript_event_codecs(operation: &str, event_type: &str) -> String {
4456 let stem = pascal_case(operation);
4457 format!(
4458 "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"
4459 )
4460}
4461
4462#[allow(clippy::too_many_lines)]
4463fn generate_typescript(contract: &ContractIr) -> String {
4464 let capability_name = pascal_case(
4465 contract
4466 .capability_id
4467 .split('@')
4468 .next()
4469 .and_then(|identity| identity.rsplit('.').next())
4470 .unwrap_or("Capability"),
4471 );
4472 let mut types = TypeScriptTypes::new();
4473 let mut clients = Vec::new();
4474 let mut providers = Vec::new();
4475 let mut errors = Vec::new();
4476 let mut codecs = Vec::new();
4477 let mut request_dispatch_arms = Vec::new();
4478 let operation_names = contract
4479 .operations
4480 .iter()
4481 .map(|operation| quote_string(&operation.name))
4482 .collect::<Vec<_>>()
4483 .join(", ");
4484 let stream_operation_names = contract
4485 .operations
4486 .iter()
4487 .filter(|operation| operation.interaction == "stream")
4488 .map(|operation| quote_string(&operation.name))
4489 .collect::<Vec<_>>()
4490 .join(", ");
4491 let event_operation_names = contract
4492 .operations
4493 .iter()
4494 .filter(|operation| operation.interaction == "event")
4495 .map(|operation| quote_string(&operation.name))
4496 .collect::<Vec<_>>()
4497 .join(", ");
4498 let has_event_operations = contract
4499 .operations
4500 .iter()
4501 .any(|operation| operation.interaction == "event");
4502 for operation in &contract.operations {
4503 let operation_name = pascal_case(&operation.name);
4504 let request_name = format!("{operation_name}Request");
4505 let response_name = format!("{operation_name}Response");
4506 let error_name = format!("{operation_name}Error");
4507 let request_type = types.type_for(&operation.request, &request_name);
4508 let response_type = types.type_for(&operation.response, &response_name);
4509 let variants = &operation.domain_errors;
4510 let error_type = if variants.is_empty() {
4511 "UnknownDomainError".to_owned()
4512 } else {
4513 let mut values = variants
4514 .iter()
4515 .map(|variant| {
4516 if let Some(payload) = &variant.payload {
4517 let payload_name = format!("{error_name}{}Payload", variant.name);
4518 let payload_type =
4519 types.type_for_non_null(payload.non_null(), &payload_name);
4520 let payload_type = if payload.is_nullable() {
4521 format!("{payload_type} | null")
4522 } else {
4523 payload_type
4524 };
4525 let payload_optional = if variant.payload_required { "" } else { "?" };
4526 format!(
4527 "{{ readonly code: {}; readonly payload{payload_optional}: {payload_type} }}",
4528 quote_string(&variant.code),
4529 )
4530 } else if variant.structured {
4531 format!("{{ readonly code: {} }}", quote_string(&variant.code))
4532 } else {
4533 quote_string(&variant.code)
4534 }
4535 })
4536 .collect::<Vec<_>>();
4537 values.push("UnknownDomainError".to_owned());
4538 values.join(" | ")
4539 };
4540 let invocation_error_name = format!("{operation_name}InvocationError");
4541 let result_name = format!("{operation_name}Result");
4542 let result_value_type = match operation.interaction.as_str() {
4543 "stream" => format!("StreamSession<{response_type}, {error_name}>"),
4544 "event" => "ReadonlyArray<EventPublishResult>".to_owned(),
4545 _ => response_type.clone(),
4546 };
4547 if operation.interaction == "event" {
4548 errors.push(format!(
4549 "export type {error_name} = {error_type};\nexport type {result_name} = ReadonlyArray<EventPublishResult>;"
4550 ));
4551 } else {
4552 errors.push(format!(
4553 "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} }};"
4554 ));
4555 }
4556 codecs.push(generate_typescript_codecs(
4557 &operation.name,
4558 &request_type,
4559 &response_type,
4560 &error_name,
4561 variants,
4562 ));
4563 if operation.interaction == "event" {
4564 codecs.push(generate_typescript_event_codecs(
4565 &operation.name,
4566 &request_type,
4567 ));
4568 }
4569 if matches!(operation.interaction.as_str(), "request" | "stream") {
4570 clients.push(format!(
4571 " {}(request: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4572 typescript_property_name(&snake_case(&operation.name)),
4573 ));
4574 providers.push(format!(
4575 " {}(context: InvocationContext, request: {request_type}): Promise<{result_name}>;",
4576 typescript_property_name(&snake_case(&operation.name)),
4577 ));
4578 } else {
4579 clients.push(format!(
4580 " {}(event: {request_type}, context?: InvocationContext): Promise<{result_name}>;",
4581 typescript_property_name(&snake_case(&operation.name)),
4582 ));
4583 providers.push(format!(
4584 " {}(context: InvocationContext, event: {request_type}): void;",
4585 typescript_property_name(&snake_case(&operation.name)),
4586 ));
4587 }
4588 if operation.interaction == "request" {
4589 let provider_method = typescript_property_name(&snake_case(&operation.name));
4590 request_dispatch_arms.push(format!(
4591 " 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 }}",
4592 quote_string(&operation.name),
4593 ));
4594 }
4595 }
4596 let mut output = String::new();
4597 output.push_str(TYPESCRIPT_HEADER);
4598 output.push_str("import * as lensoContractRuntime from \"@lenso/contract-runtime\";\n\n");
4599 writeln!(
4600 output,
4601 "export const CAPABILITY_ID = {};",
4602 quote_string(&contract.capability_id)
4603 )
4604 .expect("writing to a String cannot fail");
4605 writeln!(
4606 output,
4607 "export const DESCRIPTOR_VERSION = {};",
4608 quote_string(&contract.version)
4609 )
4610 .expect("writing to a String cannot fail");
4611 writeln!(output, "export const PORTABLE = {};", contract.portable)
4612 .expect("writing to a String cannot fail");
4613 write!(
4614 output,
4615 "export const CROSS_LANE_TRANSFER = {};\n\n",
4616 contract.cross_lane_transfer
4617 )
4618 .expect("writing to a String cannot fail");
4619 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");
4620 if has_event_operations {
4621 output.push_str("export type EventAdmission = lensoContractRuntime.EventAdmission;\nexport type EventPublishResult = lensoContractRuntime.EventPublishResult;\n\n");
4622 }
4623 for declaration in types.declarations {
4624 output.push_str(&declaration);
4625 output.push('\n');
4626 }
4627 for error in errors {
4628 output.push_str(&error);
4629 output.push('\n');
4630 }
4631 for codec in codecs {
4632 output.push_str(&codec);
4633 output.push('\n');
4634 }
4635 write!(
4636 output,
4637 "\nexport interface {capability_name}Client {{\n{}\n}}\n\nexport interface {capability_name}Provider {{\n{}\n}}\n",
4638 clients.join("\n"),
4639 providers.join("\n")
4640 )
4641 .expect("writing to a String cannot fail");
4642 write!(
4643 output,
4644 "\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",
4645 request_dispatch_arms.join("\n")
4646 )
4647 .expect("writing to a String cannot fail");
4648 output.push_str(
4649 "\nexport const portableValueProfile = lensoContractRuntime.portableValueProfile;\n",
4650 );
4651 output
4652}