Skip to main content

harmos_domain/inventory/
contracts.rs

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