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