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
16use crate::model::operations::lifecycle;
17
18// Standard Library Imports
19use std::borrow::Cow;
20use std::collections::BTreeSet;
21use std::time::Duration;
22// Third Party Imports
23use serde::{Deserialize, Serialize};
24// Local Imports
25use 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
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: 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/* === Enums === */
153
154#[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
196/* === Traits === */
197
198/* === Implementations === */
199
200impl 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
331/* === Public Functions === */
332
333pub 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
497/* === Private Functions === */
498
499const 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/* === Tests === */