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