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