1use crate::model::operations::lifecycle;
17
18use std::borrow::Cow;
20use std::collections::BTreeSet;
21use std::time::Duration;
22use serde::{Deserialize, Serialize};
24use crate::PREFIX;
26use crate::catalog::Node;
27use crate::config::SchemaSettings;
28use crate::operations::{Backoff, Contract, Entry, Jitter, OperationPolicy, ReplaySafety, RetryPolicy, Visibility, WireDeclaration};
29use crate::plugin::{Grant, Requirement};
30use crate::schema::declaration::Named;
31use crate::schema::graph::Schema;
32
33const ENTRY_LIMIT: usize = 4096;
36const GRANT_LIMIT: usize = 256;
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(try_from = "Decoded")]
46pub struct Manifest {
47 pub facility: String,
48 pub owner: String,
49 pub provides: Vec<Contract>,
50 pub requires: Vec<Required>,
51 pub lifecycle: [lifecycle::Descriptor; 2],
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct Required {
57 pub contract: Contract,
58
59 #[serde(default)]
60 pub grants: Vec<Grant>,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct Identity {
66 pub facility: String,
67 pub id: String,
68 pub version: String,
69 pub description: String,
70 pub config: String,
71}
72
73#[derive(Deserialize)]
74#[serde(deny_unknown_fields)]
75struct Decoded {
76 #[serde(default)]
77 kind: Option<String>,
78
79 facility: String,
80 owner: String,
81 provides: Vec<Contract>,
82 requires: Vec<Required>,
83 lifecycle: [lifecycle::Descriptor; 2],
84}
85
86#[derive(Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct NamedContract {
90 pub operation: NamedOperation,
91 pub identity: String,
92 pub payload_codec: String,
93 pub response_codec: String,
94 pub input_codec: Option<String>,
95 pub output_codec: Option<String>,
96 pub codec: crate::operations::Codec,
97 pub request: String,
98 pub response: String,
99
100 #[serde(default)]
101 pub input: Option<String>,
102
103 #[serde(default)]
104 pub output: Option<String>,
105
106 #[serde(default)]
107 pub grpc: Option<crate::operations::Grpc>,
108}
109
110#[derive(Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct NamedRequired {
113 pub contract: NamedContract,
114
115 #[serde(default)]
116 pub grants: Vec<Grant>,
117}
118
119#[derive(Deserialize)]
120#[serde(deny_unknown_fields)]
121pub struct NamedManifest {
122 pub facility: String,
123 pub owner: String,
124 pub provides: Vec<NamedContract>,
125 pub requires: Vec<NamedRequired>,
126 pub lifecycle: [NamedLifecycle; 2],
127}
128
129#[derive(Deserialize)]
130#[serde(deny_unknown_fields)]
131pub struct NamedOperation {
132 pub id: crate::Identifier,
133 pub description: String,
134 pub policy: OperationPolicy,
135 pub visibility: Visibility,
136 pub replay: ReplaySafety,
137}
138
139#[derive(Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct NamedLifecycle {
142 pub action: lifecycle::Action,
143 pub identity: String,
144 pub policy: OperationPolicy,
145}
146
147pub(crate) struct Encoder<const N: usize> {
148 pub(crate) bytes: [u8; N],
149 pub(crate) cursor: usize,
150}
151
152#[derive(Debug, thiserror::Error)]
155pub enum Error {
156 #[error("missing complete {facility} contract inventory for `{owner}`")]
157 Missing { facility: String, owner: String },
158
159 #[error("multiple {facility} contract inventories for `{owner}`")]
160 Duplicate { facility: String, owner: String },
161
162 #[error("contract inventory exceeds its entry or fragment budget")]
163 Budget,
164
165 #[error("contract inventory requires a guest or sidecar facility and a valid artifact owner")]
166 Identity,
167
168 #[error("contract inventory has an invalid fragment kind")]
169 Kind,
170
171 #[error("requirement `{id}` carries a duplicate or invalid grant")]
172 Grant { id: String },
173
174 #[error("duplicate ordinary operation `{id}` in {direction}")]
175 OperationDuplicate { direction: &'static str, id: String },
176
177 #[error("invalid ordinary operation metadata: {0}")]
178 Operation(#[from] crate::Error),
179
180 #[error("SDK lifecycle inventory must contain initialization then shutdown")]
181 LifecycleOrder,
182
183 #[error("invalid SDK lifecycle inventory: {0}")]
184 Lifecycle(#[from] lifecycle::Error),
185
186 #[error("invalid shared contract schema: {0}")]
187 Schema(#[from] crate::schema::graph::Error),
188
189 #[error("invalid resolved contract: {0}")]
190 Contract(#[from] crate::operations::ContractError),
191
192 #[error("invalid contract inventory JSON: {0}")]
193 Decode(#[from] serde_json::Error),
194}
195
196impl NamedContract {
201 pub fn resolve(self, declarations: &[Named], settings: &mut SchemaSettings) -> Result<Contract, Error> {
202 let mut roots = vec![("request", self.request.as_str()), ("response", self.response.as_str())];
203 if let Some(input) = &self.input {
204 roots.push(("input", input.as_str()));
205 }
206 if let Some(output) = &self.output {
207 roots.push(("output", output.as_str()));
208 }
209 let schema = Schema::from_named_roots(&roots, declarations, settings)?;
210 let schema = super::retain_schema(schema, settings)?;
211 if self.identity != "harmos.operation.v1" {
212 return Err(Error::Identity);
213 }
214 for codec in [Some(self.payload_codec), Some(self.response_codec), self.input_codec, self.output_codec].into_iter().flatten() {
215 if codec != "harmos.postcard.v1" {
216 return Err(crate::operations::ContractError::Codec { codec }.into());
217 }
218 }
219 let fingerprint = crate::schema::identity::contract(self.operation.id.as_str(), &schema, self.codec.id())?;
220 let operation = crate::OperationDefinition {
221 id: self.operation.id,
222 description: self.operation.description,
223 policy: self.operation.policy,
224 visibility: self.operation.visibility,
225 replay: self.operation.replay,
226 fingerprint,
227 };
228 Ok(Contract::new(operation, fingerprint, self.codec, schema)?.with_grpc(self.grpc)?)
229 }
230}
231
232impl NamedManifest {
233 pub fn resolve(self, declarations: &[Named], settings: &mut SchemaSettings) -> Result<Manifest, Error> {
234 if self.provides.len() > ENTRY_LIMIT || self.requires.len() > ENTRY_LIMIT {
235 return Err(Error::Budget);
236 }
237 let provides = self.provides.into_iter().map(|contract| contract.resolve(declarations, settings)).collect::<Result<_, _>>()?;
238 let requires = self
239 .requires
240 .into_iter()
241 .map(|entry| {
242 Ok(Required {
243 contract: entry.contract.resolve(declarations, settings)?,
244 grants: entry.grants,
245 })
246 })
247 .collect::<Result<_, Error>>()?;
248 let resolve = |entry: NamedLifecycle| -> Result<lifecycle::Descriptor, Error> {
249 if entry.identity != "harmos.operation.v1" {
250 return Err(Error::Identity);
251 }
252 Ok(lifecycle::Descriptor::new(entry.action, entry.policy)?)
253 };
254 let [initialize, shutdown] = self.lifecycle;
255 let lifecycle = [resolve(initialize)?, resolve(shutdown)?];
256 let manifest = Manifest {
257 facility: self.facility,
258 owner: self.owner,
259 provides,
260 requires,
261 lifecycle,
262 };
263 validate(&manifest)?;
264 Ok(manifest)
265 }
266}
267
268impl TryFrom<Decoded> for Manifest {
269 type Error = Error;
270
271 fn try_from(value: Decoded) -> Result<Self, Error> {
272 if value.kind.as_deref().is_some_and(|kind| kind != "contracts") {
273 return Err(Error::Kind);
274 }
275 let manifest = Self {
276 facility: value.facility,
277 owner: value.owner,
278 provides: value.provides,
279 requires: value.requires,
280 lifecycle: value.lifecycle,
281 };
282 validate(&manifest)?;
283 Ok(manifest)
284 }
285}
286
287impl Required {
288 pub fn from_tree(entries: &[Node<Requirement>]) -> Result<Vec<Self>, crate::operations::ContractError> {
289 let mut required = Vec::with_capacity(crate::catalog::count(entries));
290 crate::catalog::try_for_each(entries, |entry| {
291 required.push(Self {
292 contract: entry.declaration.contract()?,
293 grants: entry.grants.to_vec(),
294 });
295 Ok::<(), crate::operations::ContractError>(())
296 })?;
297 Ok(required)
298 }
299
300 pub fn grants(&self, grant: &Grant) -> bool {
301 self.grants.contains(grant)
302 }
303}
304
305impl<const N: usize> Encoder<N> {
306 pub(crate) const fn raw(&mut self, bytes: &[u8]) {
307 if N == 0 {
308 self.cursor += bytes.len();
309 } else {
310 super::write(&mut self.bytes, &mut self.cursor, bytes);
311 }
312 }
313
314 pub(crate) const fn quoted(&mut self, value: &str) {
315 if N == 0 {
316 self.cursor += super::quoted_len(value);
317 } else {
318 super::quoted(&mut self.bytes, &mut self.cursor, value);
319 }
320 }
321
322 pub(crate) const fn number(&mut self, value: u64) {
323 if N == 0 {
324 self.cursor += super::digits(value);
325 } else {
326 super::number(&mut self.bytes, &mut self.cursor, value);
327 }
328 }
329}
330
331pub fn decode(fragment: &[u8]) -> Result<Manifest, Error> {
334 if fragment.len() > crate::FRAGMENT_LIMIT {
335 return Err(Error::Budget);
336 }
337 Ok(serde_json::from_slice(fragment)?)
338}
339
340pub fn select(manifests: Vec<Manifest>, facility: &str, owner: &str) -> Result<Manifest, Error> {
341 let mut matching = manifests.into_iter().filter(|value| value.facility == facility && value.owner == owner);
342 let manifest = matching.next().ok_or_else(|| Error::Missing {
343 facility: facility.to_owned(),
344 owner: owner.to_owned(),
345 })?;
346 if matching.next().is_some() {
347 return Err(Error::Duplicate {
348 facility: facility.to_owned(),
349 owner: owner.to_owned(),
350 });
351 }
352 validate(&manifest)?;
353 Ok(manifest)
354}
355
356pub fn validate(manifest: &Manifest) -> Result<(), Error> {
357 if !valid_identity(&manifest.facility, &manifest.owner) {
358 return Err(Error::Identity);
359 }
360 let required = manifest.requires.iter().map(|required| &required.contract);
361 for (direction, contracts) in [("provides", manifest.provides.iter().collect::<Vec<_>>()), ("requires", required.collect())] {
362 if contracts.len() > ENTRY_LIMIT {
363 return Err(Error::Budget);
364 }
365 let mut ids = BTreeSet::new();
366 for contract in contracts {
367 contract.operation().validate()?;
368 if !ids.insert(&contract.operation().id) {
369 return Err(Error::OperationDuplicate {
370 direction,
371 id: contract.operation().id.to_string(),
372 });
373 }
374 }
375 }
376 let grants = manifest.requires.iter().map(|required| required.grants.len()).sum::<usize>();
377 if grants > ENTRY_LIMIT {
378 return Err(Error::Budget);
379 }
380 for required in &manifest.requires {
381 let mut seen = BTreeSet::new();
382 let operation = &required.contract.operation().id;
383 for grant in &required.grants {
384 if grant.operation() != operation.as_str()
385 || grant.scope().is_none_or(|scope| scope.is_empty() || scope.len() > GRANT_LIMIT)
386 || !seen.insert(grant)
387 {
388 return Err(Error::Grant {
389 id: required.contract.operation().id.to_string(),
390 });
391 }
392 }
393 }
394 if manifest.lifecycle[0].action() != lifecycle::Action::Initialize || manifest.lifecycle[1].action() != lifecycle::Action::Shutdown {
395 return Err(Error::LifecycleOrder);
396 }
397 Ok(())
398}
399
400pub const fn encoded_tree_len(
401 facility: &str, owner: &str, provides: &[Node<WireDeclaration>], requires: &[Node<Requirement>], lifecycle: &[OperationPolicy; 2],
402) -> usize {
403 let size = encoded::<0>(facility, owner, provides, requires, lifecycle).cursor;
404 assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
405 size
406}
407
408pub const fn encode_tree<const N: usize>(
409 facility: &str, owner: &str, provides: &[Node<WireDeclaration>], requires: &[Node<Requirement>], lifecycle: &[OperationPolicy; 2],
410) -> [u8; N] {
411 assert!(N == encoded_tree_len(facility, owner, provides, requires, lifecycle));
412 encoded::<N>(facility, owner, provides, requires, lifecycle).bytes
413}
414
415pub const fn identity_len(facility: &str, id: &str, version: &str, description: &str, config: &str) -> usize {
416 let size = encoded_identity::<0>(facility, id, version, description, config).cursor;
417 assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
418 size
419}
420
421pub const fn encode_identity<const N: usize>(facility: &str, id: &str, version: &str, description: &str, config: &str) -> [u8; N] {
422 assert!(N == identity_len(facility, id, version, description, config));
423 encoded_identity::<N>(facility, id, version, description, config).bytes
424}
425
426pub(crate) const fn valid_identity(facility: &str, owner: &str) -> bool {
427 matches!(facility.as_bytes(), b"guest" | b"sidecar") && crate::artifact_subset(owner)
428}
429
430pub(crate) const fn wire<const N: usize>(out: &mut Encoder<N>, entry: &WireDeclaration) {
431 out.raw(b"{\"operation\":");
432 declaration(out, &entry.operation);
433 out.raw(b",\"identity\":\"harmos.operation.v1\",\"payload_codec\":");
434 out.quoted(entry.codec);
435 out.raw(b",\"response_codec\":");
436 out.quoted(entry.response_codec);
437 if let Some(codec) = entry.input_codec {
438 out.raw(b",\"input_codec\":");
439 out.quoted(codec);
440 }
441 if let Some(codec) = entry.output_codec {
442 out.raw(b",\"output_codec\":");
443 out.quoted(codec);
444 }
445 out.raw(b",\"codec\":\"postcard\",\"request\":");
446 out.quoted(entry.request.name);
447 out.raw(b",\"response\":");
448 out.quoted(entry.response.name);
449 if let Some(input) = entry.input {
450 out.raw(b",\"input\":");
451 out.quoted(input.name);
452 }
453 if let Some(output) = entry.output {
454 out.raw(b",\"output\":");
455 out.quoted(output.name);
456 }
457 if let Some(binding) = &entry.grpc {
458 out.raw(b",\"grpc\":{\"package\":");
459 out.quoted(match &binding.package {
460 Cow::Borrowed(value) => value,
461 Cow::Owned(_) => panic!("const gRPC package must be borrowed"),
462 });
463 out.raw(b",\"service\":");
464 out.quoted(match &binding.service {
465 Cow::Borrowed(value) => value,
466 Cow::Owned(_) => panic!("const gRPC service must be borrowed"),
467 });
468 out.raw(b",\"method\":");
469 out.quoted(match &binding.method {
470 Cow::Borrowed(value) => value,
471 Cow::Owned(_) => panic!("const gRPC method must be borrowed"),
472 });
473 out.raw(b"}");
474 }
475 out.raw(b"}");
476}
477
478pub(crate) const fn policy<const N: usize>(out: &mut Encoder<N>, value: OperationPolicy) {
479 out.raw(b"{\"timeout\":");
480 duration(out, value.timeout);
481 out.raw(b",\"attempt_timeout\":");
482 duration(out, value.attempt_timeout);
483 out.raw(b",\"retries\":");
484 match value.retries {
485 RetryPolicy::Never => out.quoted("never"),
486 RetryPolicy::OnTransient { max_retries, backoff: value } => {
487 out.raw(b"{\"on_transient\":{\"max_retries\":");
488 out.number(max_retries as u64);
489 out.raw(b",\"backoff\":");
490 backoff(out, value);
491 out.raw(b"}}");
492 }
493 }
494 out.raw(b"}");
495}
496
497const fn encoded_identity<const N: usize>(facility: &str, id: &str, version: &str, description: &str, config: &str) -> Encoder<N> {
500 assert!(valid_identity(facility, id));
501 let mut out = Encoder { bytes: [0; N], cursor: 0 };
502 out.raw(PREFIX);
503 out.raw(b"{\"kind\":\"plugin\",\"facility\":");
504 out.quoted(facility);
505 out.raw(b",\"id\":");
506 out.quoted(id);
507 out.raw(b",\"version\":");
508 out.quoted(version);
509 out.raw(b",\"description\":");
510 out.quoted(description);
511 out.raw(b",\"config\":");
512 out.quoted(config);
513 out.raw(b"}\0");
514 out
515}
516
517const fn encoded<const N: usize>(
518 facility: &str, owner: &str, provides: &[Node<WireDeclaration>], requires: &[Node<Requirement>], lifecycle: &[OperationPolicy; 2],
519) -> Encoder<N> {
520 assert!(valid_identity(facility, owner));
521 assert!(crate::catalog::count(provides) <= ENTRY_LIMIT);
522 assert!(crate::catalog::count(requires) <= ENTRY_LIMIT);
523 let mut out = Encoder { bytes: [0; N], cursor: 0 };
524 out.raw(PREFIX);
525 out.raw(b"{\"kind\":\"contracts\",\"facility\":");
526 out.quoted(facility);
527 out.raw(b",\"owner\":");
528 out.quoted(owner);
529 out.raw(b",\"provides\":[");
530 tree(&mut out, provides, &mut true);
531 out.raw(b"],\"requires\":[");
532 required(&mut out, requires, &mut true);
533 out.raw(b"],\"lifecycle\":[{\"action\":\"initialize\",\"identity\":\"harmos.operation.v1\",\"policy\":");
534 policy(&mut out, lifecycle[0]);
535 out.raw(b"},{\"action\":\"shutdown\",\"identity\":\"harmos.operation.v1\",\"policy\":");
536 policy(&mut out, lifecycle[1]);
537 out.raw(b"}]}\0");
538 out
539}
540
541const fn required<const N: usize>(out: &mut Encoder<N>, entries: &[Node<Requirement>], first: &mut bool) {
542 let mut index = 0;
543 while index < entries.len() {
544 match &entries[index] {
545 Node::Leaf(entry) => {
546 if !*first {
547 out.raw(b",");
548 }
549 *first = false;
550 out.raw(b"{\"contract\":");
551 wire(out, &entry.declaration);
552 out.raw(b",\"grants\":[");
553 let mut grant = 0;
554 while grant < entry.grants.len() {
555 if grant > 0 {
556 out.raw(b",");
557 }
558 let (operation, scope) = entry.grants[grant].borrowed();
559 assert!(
560 super::publications::same_bytes(operation, entry.declaration.operation.id),
561 "a requirement grants only its own operation"
562 );
563 let Some(scope) = scope else {
564 panic!("a requirement grant names its scope");
565 };
566 assert!(!scope.is_empty() && scope.len() <= GRANT_LIMIT, "grant scopes are nonempty and bounded");
567 out.raw(b"{\"operation\":");
568 out.quoted(operation);
569 out.raw(b",\"scope\":");
570 out.quoted(scope);
571 out.raw(b"}");
572 grant += 1;
573 }
574 out.raw(b"]}");
575 }
576 Node::Nested(nested) => required(out, nested, first),
577 }
578 index += 1;
579 }
580}
581
582const fn tree<const N: usize>(out: &mut Encoder<N>, entries: &[Node<WireDeclaration>], first: &mut bool) {
583 let mut index = 0;
584 while index < entries.len() {
585 match &entries[index] {
586 Node::Leaf(entry) => {
587 if !*first {
588 out.raw(b",");
589 }
590 *first = false;
591 wire(out, entry);
592 }
593 Node::Nested(nested) => tree(out, nested, first),
594 }
595 index += 1;
596 }
597}
598
599const fn declaration<const N: usize>(out: &mut Encoder<N>, entry: &Entry) {
600 out.raw(b"{\"id\":");
601 out.quoted(entry.id);
602 out.raw(b",\"description\":");
603 out.quoted(entry.description);
604 out.raw(b",\"policy\":");
605 policy(out, entry.policy);
606 out.raw(b",\"visibility\":");
607 out.quoted(match entry.visibility {
608 Visibility::Private => "private",
609 Visibility::Shared => "shared",
610 Visibility::Host => "host",
611 });
612 out.raw(b",\"replay\":");
613 out.quoted(match entry.replay {
614 ReplaySafety::Unknown => "unknown",
615 ReplaySafety::Idempotent => "idempotent",
616 ReplaySafety::Deduplicated => "deduplicated",
617 });
618 out.raw(b"}");
619}
620
621const fn duration<const N: usize>(out: &mut Encoder<N>, value: Duration) {
622 out.raw(b"{\"secs\":");
623 out.number(value.as_secs());
624 out.raw(b",\"nanos\":");
625 out.number(value.subsec_nanos() as u64);
626 out.raw(b"}");
627}
628
629const fn backoff<const N: usize>(out: &mut Encoder<N>, value: Backoff) {
630 let jitter = match value {
631 Backoff::Fixed { delay, jitter } => {
632 out.raw(b"{\"fixed\":{\"delay\":");
633 duration(out, delay);
634 jitter
635 }
636 Backoff::Exponential { initial, maximum, jitter } => {
637 out.raw(b"{\"exponential\":{\"initial\":");
638 duration(out, initial);
639 out.raw(b",\"maximum\":");
640 duration(out, maximum);
641 jitter
642 }
643 };
644 out.raw(b",\"jitter\":");
645 out.quoted(match jitter {
646 Jitter::None => "none",
647 Jitter::Deterministic => "deterministic",
648 });
649 out.raw(b"}}");
650}
651
652