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    Ok(())
326}
327
328pub const fn encoded_tree_len(
329    facility: &str, owner: &str, provides: &[Node<WireDeclaration>], requires: &[Node<Requirement>], lifecycle: &[OperationPolicy; 2],
330) -> usize {
331    let size = encoded::<0>(facility, owner, provides, requires, lifecycle).cursor;
332    assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
333    size
334}
335
336pub const fn encode_tree<const N: usize>(
337    facility: &str, owner: &str, provides: &[Node<WireDeclaration>], requires: &[Node<Requirement>], lifecycle: &[OperationPolicy; 2],
338) -> [u8; N] {
339    assert!(N == encoded_tree_len(facility, owner, provides, requires, lifecycle));
340    encoded::<N>(facility, owner, provides, requires, lifecycle).bytes
341}
342
343pub const fn identity_len(facility: &str, id: &str, version: &str, description: &str, config: &str) -> usize {
344    let size = encoded_identity::<0>(facility, id, version, description, config).cursor;
345    assert!(size - PREFIX.len() - 1 <= crate::FRAGMENT_LIMIT);
346    size
347}
348
349pub const fn encode_identity<const N: usize>(facility: &str, id: &str, version: &str, description: &str, config: &str) -> [u8; N] {
350    assert!(N == identity_len(facility, id, version, description, config));
351    encoded_identity::<N>(facility, id, version, description, config).bytes
352}
353
354pub(crate) const fn valid_identity(facility: &str, owner: &str) -> bool {
355    matches!(facility.as_bytes(), b"guest" | b"sidecar") && crate::artifact_subset(owner)
356}
357
358pub(crate) const fn wire<const N: usize>(out: &mut Encoder<N>, entry: &WireDeclaration) {
359    out.raw(b"{\"operation\":");
360    declaration(out, &entry.operation);
361    out.raw(b",\"version\":");
362    out.number(entry.version as u64);
363    out.raw(b",\"codec\":\"postcard\",\"request\":");
364    out.quoted(entry.request.name);
365    out.raw(b",\"response\":");
366    out.quoted(entry.response.name);
367    if let Some(input) = entry.input {
368        out.raw(b",\"input\":");
369        out.quoted(input.name);
370    }
371    if let Some(output) = entry.output {
372        out.raw(b",\"output\":");
373        out.quoted(output.name);
374    }
375    if let Some(binding) = &entry.grpc {
376        out.raw(b",\"grpc\":{\"package\":");
377        out.quoted(match &binding.package {
378            Cow::Borrowed(value) => value,
379            Cow::Owned(_) => panic!("const gRPC package must be borrowed"),
380        });
381        out.raw(b",\"service\":");
382        out.quoted(match &binding.service {
383            Cow::Borrowed(value) => value,
384            Cow::Owned(_) => panic!("const gRPC service must be borrowed"),
385        });
386        out.raw(b",\"method\":");
387        out.quoted(match &binding.method {
388            Cow::Borrowed(value) => value,
389            Cow::Owned(_) => panic!("const gRPC method must be borrowed"),
390        });
391        out.raw(b"}");
392    }
393    out.raw(b"}");
394}
395
396pub(crate) const fn policy<const N: usize>(out: &mut Encoder<N>, value: OperationPolicy) {
397    out.raw(b"{\"timeout\":");
398    duration(out, value.timeout);
399    out.raw(b",\"attempt_timeout\":");
400    duration(out, value.attempt_timeout);
401    out.raw(b",\"retries\":");
402    match value.retries {
403        RetryPolicy::Never => out.quoted("never"),
404        RetryPolicy::OnTransient { max_retries, backoff: value } => {
405            out.raw(b"{\"on_transient\":{\"max_retries\":");
406            out.number(max_retries as u64);
407            out.raw(b",\"backoff\":");
408            backoff(out, value);
409            out.raw(b"}}");
410        }
411    }
412    out.raw(b",\"restart\":");
413    match value.restart {
414        RestartPolicy::Never => out.quoted("never"),
415        RestartPolicy::OnTransient { credit, sustained_health, backoff: value } => {
416            out.raw(b"{\"on_transient\":{\"credit\":");
417            out.number(credit as u64);
418            out.raw(b",\"sustained_health\":");
419            duration(out, sustained_health);
420            out.raw(b",\"backoff\":");
421            backoff(out, value);
422            out.raw(b"}}");
423        }
424    }
425    out.raw(b"}");
426}
427
428/* === Private Functions === */
429
430const fn encoded_identity<const N: usize>(facility: &str, id: &str, version: &str, description: &str, config: &str) -> Encoder<N> {
431    assert!(valid_identity(facility, id));
432    let mut out = Encoder { bytes: [0; N], cursor: 0 };
433    out.raw(PREFIX);
434    out.raw(b"{\"kind\":\"plugin\",\"facility\":");
435    out.quoted(facility);
436    out.raw(b",\"id\":");
437    out.quoted(id);
438    out.raw(b",\"version\":");
439    out.quoted(version);
440    out.raw(b",\"description\":");
441    out.quoted(description);
442    out.raw(b",\"config\":");
443    out.quoted(config);
444    out.raw(b"}\0");
445    out
446}
447
448const fn encoded<const N: usize>(
449    facility: &str, owner: &str, provides: &[Node<WireDeclaration>], requires: &[Node<Requirement>], lifecycle: &[OperationPolicy; 2],
450) -> Encoder<N> {
451    assert!(valid_identity(facility, owner));
452    assert!(crate::catalog::count(provides) <= ENTRY_LIMIT);
453    assert!(crate::catalog::count(requires) <= ENTRY_LIMIT);
454    let mut out = Encoder { bytes: [0; N], cursor: 0 };
455    out.raw(PREFIX);
456    out.raw(b"{\"kind\":\"contracts\",\"facility\":");
457    out.quoted(facility);
458    out.raw(b",\"owner\":");
459    out.quoted(owner);
460    out.raw(b",\"provides\":[");
461    tree(&mut out, provides, &mut true);
462    out.raw(b"],\"requires\":[");
463    required(&mut out, requires, &mut true);
464    out.raw(b"],\"lifecycle\":[{\"action\":\"initialize\",\"version\":1,\"policy\":");
465    policy(&mut out, lifecycle[0]);
466    out.raw(b"},{\"action\":\"shutdown\",\"version\":1,\"policy\":");
467    policy(&mut out, lifecycle[1]);
468    out.raw(b"}]}\0");
469    out
470}
471
472const fn required<const N: usize>(out: &mut Encoder<N>, entries: &[Node<Requirement>], first: &mut bool) {
473    let mut index = 0;
474    while index < entries.len() {
475        match &entries[index] {
476            Node::Leaf(entry) => {
477                if !*first {
478                    out.raw(b",");
479                }
480                *first = false;
481                out.raw(b"{\"contract\":");
482                wire(out, &entry.declaration);
483                out.raw(b",\"grants\":[");
484                let mut grant = 0;
485                while grant < entry.grants.len() {
486                    if grant > 0 {
487                        out.raw(b",");
488                    }
489                    let (operation, scope) = entry.grants[grant].borrowed();
490                    assert!(
491                        super::publications::same_bytes(operation, entry.declaration.operation.id),
492                        "a requirement grants only its own operation"
493                    );
494                    let Some(scope) = scope else {
495                        panic!("a requirement grant names its scope");
496                    };
497                    assert!(!scope.is_empty() && scope.len() <= GRANT_LIMIT, "grant scopes are nonempty and bounded");
498                    out.raw(b"{\"operation\":");
499                    out.quoted(operation);
500                    out.raw(b",\"scope\":");
501                    out.quoted(scope);
502                    out.raw(b"}");
503                    grant += 1;
504                }
505                out.raw(b"]}");
506            }
507            Node::Nested(nested) => required(out, nested, first),
508        }
509        index += 1;
510    }
511}
512
513const fn tree<const N: usize>(out: &mut Encoder<N>, entries: &[Node<WireDeclaration>], first: &mut bool) {
514    let mut index = 0;
515    while index < entries.len() {
516        match &entries[index] {
517            Node::Leaf(entry) => {
518                if !*first {
519                    out.raw(b",");
520                }
521                *first = false;
522                wire(out, entry);
523            }
524            Node::Nested(nested) => tree(out, nested, first),
525        }
526        index += 1;
527    }
528}
529
530const fn declaration<const N: usize>(out: &mut Encoder<N>, entry: &Entry) {
531    out.raw(b"{\"id\":");
532    out.quoted(entry.id);
533    out.raw(b",\"description\":");
534    out.quoted(entry.description);
535    out.raw(b",\"policy\":");
536    policy(out, entry.policy);
537    out.raw(b",\"visibility\":");
538    out.quoted(match entry.visibility {
539        Visibility::Private => "private",
540        Visibility::Shared => "shared",
541        Visibility::Host => "host",
542    });
543    out.raw(b",\"replay\":");
544    out.quoted(match entry.replay {
545        ReplaySafety::Unknown => "unknown",
546        ReplaySafety::Idempotent => "idempotent",
547        ReplaySafety::Deduplicated => "deduplicated",
548    });
549    out.raw(b"}");
550}
551
552const fn duration<const N: usize>(out: &mut Encoder<N>, value: Duration) {
553    out.raw(b"{\"secs\":");
554    out.number(value.as_secs());
555    out.raw(b",\"nanos\":");
556    out.number(value.subsec_nanos() as u64);
557    out.raw(b"}");
558}
559
560const fn backoff<const N: usize>(out: &mut Encoder<N>, value: Backoff) {
561    let jitter = match value {
562        Backoff::Fixed { delay, jitter } => {
563            out.raw(b"{\"fixed\":{\"delay\":");
564            duration(out, delay);
565            jitter
566        }
567        Backoff::Exponential { initial, maximum, jitter } => {
568            out.raw(b"{\"exponential\":{\"initial\":");
569            duration(out, initial);
570            out.raw(b",\"maximum\":");
571            duration(out, maximum);
572            jitter
573        }
574    };
575    out.raw(b",\"jitter\":");
576    out.quoted(match jitter {
577        Jitter::None => "none",
578        Jitter::Deterministic => "deterministic",
579    });
580    out.raw(b"}}");
581}
582
583/* === Tests === */