a3s_flow/
runtime_build.rs1use serde::de::Error as _;
2use serde::{Deserialize, Deserializer, Serialize};
3use std::collections::BTreeSet;
4use std::fmt;
5use std::str::FromStr;
6
7use crate::error::{FlowError, Result};
8
9const MAX_RUNTIME_BUILD_ID_BYTES: usize = 128;
10
11#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
18#[serde(transparent)]
19pub struct RuntimeBuildId(String);
20
21impl RuntimeBuildId {
22 pub fn new(value: impl Into<String>) -> Result<Self> {
24 let value = value.into();
25 validate_runtime_build_id(&value)?;
26 Ok(Self(value))
27 }
28
29 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33}
34
35impl fmt::Display for RuntimeBuildId {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 formatter.write_str(&self.0)
38 }
39}
40
41impl AsRef<str> for RuntimeBuildId {
42 fn as_ref(&self) -> &str {
43 self.as_str()
44 }
45}
46
47impl FromStr for RuntimeBuildId {
48 type Err = FlowError;
49
50 fn from_str(value: &str) -> Result<Self> {
51 Self::new(value)
52 }
53}
54
55impl<'de> Deserialize<'de> for RuntimeBuildId {
56 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
57 where
58 D: Deserializer<'de>,
59 {
60 let value = String::deserialize(deserializer)?;
61 Self::new(value).map_err(D::Error::custom)
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct RuntimeBuildCompatibility {
73 current_build_id: RuntimeBuildId,
74 compatible_build_ids: BTreeSet<RuntimeBuildId>,
75 accepts_unpinned: bool,
76}
77
78impl RuntimeBuildCompatibility {
79 pub fn new(current_build_id: RuntimeBuildId) -> Self {
81 let mut compatible_build_ids = BTreeSet::new();
82 compatible_build_ids.insert(current_build_id.clone());
83 Self {
84 current_build_id,
85 compatible_build_ids,
86 accepts_unpinned: false,
87 }
88 }
89
90 pub fn with_compatible_build(mut self, build_id: RuntimeBuildId) -> Self {
92 self.compatible_build_ids.insert(build_id);
93 self
94 }
95
96 pub fn accept_unpinned(mut self) -> Self {
98 self.accepts_unpinned = true;
99 self
100 }
101
102 pub fn current_build_id(&self) -> &RuntimeBuildId {
104 &self.current_build_id
105 }
106
107 pub fn compatible_build_ids(&self) -> impl Iterator<Item = &RuntimeBuildId> {
109 self.compatible_build_ids.iter()
110 }
111
112 pub fn accepts_unpinned(&self) -> bool {
114 self.accepts_unpinned
115 }
116
117 pub fn supports(&self, required_build_id: Option<&RuntimeBuildId>) -> bool {
119 match required_build_id {
120 Some(build_id) => self.compatible_build_ids.contains(build_id),
121 None => self.accepts_unpinned,
122 }
123 }
124}
125
126fn validate_runtime_build_id(value: &str) -> Result<()> {
127 if value.is_empty() {
128 return Err(FlowError::InvalidRuntimeBuildId(
129 "runtime build id must not be empty".to_string(),
130 ));
131 }
132 if value.len() > MAX_RUNTIME_BUILD_ID_BYTES {
133 return Err(FlowError::InvalidRuntimeBuildId(format!(
134 "runtime build id must not exceed {MAX_RUNTIME_BUILD_ID_BYTES} bytes"
135 )));
136 }
137 if !value.is_ascii() {
138 return Err(FlowError::InvalidRuntimeBuildId(
139 "runtime build id must contain only ASCII characters".to_string(),
140 ));
141 }
142 if !value
143 .as_bytes()
144 .first()
145 .is_some_and(u8::is_ascii_alphanumeric)
146 || !value
147 .as_bytes()
148 .last()
149 .is_some_and(u8::is_ascii_alphanumeric)
150 {
151 return Err(FlowError::InvalidRuntimeBuildId(
152 "runtime build id must start and end with an ASCII alphanumeric character".to_string(),
153 ));
154 }
155 if !value
156 .bytes()
157 .all(|byte| byte.is_ascii_alphanumeric() || b"-_.:+/@".contains(&byte))
158 {
159 return Err(FlowError::InvalidRuntimeBuildId(
160 "runtime build id contains an unsupported character".to_string(),
161 ));
162 }
163 Ok(())
164}