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