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