behavior_contracts/
envelope.rs1use serde_json::Value as J;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum EnvelopeFailureCode {
11 MissingVersion,
12 MalformedVersion,
13 UnsupportedMajor,
14 UnsupportedMinor,
15}
16
17impl EnvelopeFailureCode {
18 pub fn as_str(self) -> &'static str {
19 match self {
20 EnvelopeFailureCode::MissingVersion => "MISSING_VERSION",
21 EnvelopeFailureCode::MalformedVersion => "MALFORMED_VERSION",
22 EnvelopeFailureCode::UnsupportedMajor => "UNSUPPORTED_MAJOR",
23 EnvelopeFailureCode::UnsupportedMinor => "UNSUPPORTED_MINOR",
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
29pub struct EnvelopeFailure {
30 pub code: EnvelopeFailureCode,
31 pub message: String,
32}
33
34impl std::fmt::Display for EnvelopeFailure {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{}: {}", self.code.as_str(), self.message)
37 }
38}
39impl std::error::Error for EnvelopeFailure {}
40
41fn fail<T>(code: EnvelopeFailureCode, message: impl Into<String>) -> Result<T, EnvelopeFailure> {
42 Err(EnvelopeFailure {
43 code,
44 message: message.into(),
45 })
46}
47
48fn parse_version(version: &str, label: &str) -> Result<(u64, u64), EnvelopeFailure> {
49 let parts: Vec<&str> = version.split('.').collect();
50 if parts.len() != 2 {
51 return fail(
52 EnvelopeFailureCode::MalformedVersion,
53 format!("{label}: version '{version}' must be '<major>.<minor>'"),
54 );
55 }
56 let is_int = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit());
57 if !is_int(parts[0]) || !is_int(parts[1]) {
58 return fail(
59 EnvelopeFailureCode::MalformedVersion,
60 format!("{label}: version '{version}' must be two integers '<major>.<minor>'"),
61 );
62 }
63 Ok((parts[0].parse().unwrap(), parts[1].parse().unwrap()))
64}
65
66pub fn validate_envelope(
70 envelope: &J,
71 supported: &str,
72 field: Option<&str>,
73 label: Option<&str>,
74) -> Result<(), EnvelopeFailure> {
75 let field = field.unwrap_or("specVersion");
76 let label = label.unwrap_or("envelope");
77 let obj = match envelope {
78 J::Object(o) => o,
79 _ => {
80 return fail(
81 EnvelopeFailureCode::MissingVersion,
82 format!("{label}: not an object"),
83 )
84 }
85 };
86 let raw = match obj.get(field) {
87 Some(J::String(s)) => s,
88 _ => {
89 return fail(
90 EnvelopeFailureCode::MissingVersion,
91 format!("{label}: missing string '{field}'"),
92 )
93 }
94 };
95
96 let (gmaj, gmin) = parse_version(raw, label)?;
97 let (wmaj, wmin) = parse_version(supported, "supported")?;
98
99 if gmaj != wmaj {
100 return fail(
101 EnvelopeFailureCode::UnsupportedMajor,
102 format!("{label}: version {raw} has major {gmaj} but runtime supports major {wmaj}"),
103 );
104 }
105 if gmin > wmin {
106 return fail(
107 EnvelopeFailureCode::UnsupportedMinor,
108 format!("{label}: version {raw} has minor {gmin} > supported minor {wmin}"),
109 );
110 }
111 Ok(())
112}