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