1use std::{fmt, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5const MAX_OPERATION_NAME_CHARS: usize = 128;
6const MAX_TARGET_VALUE_CHARS: usize = 1_024;
7const MAX_TARGET_DEPTH: usize = 8;
8const MAX_IDEMPOTENCY_KEY_CHARS: usize = 256;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(transparent)]
14pub struct OperationName(String);
15
16impl OperationName {
17 pub fn new(value: impl Into<String>) -> Result<Self, OperationNameError> {
19 let value = value.into();
20 if valid_operation_name(&value) {
21 Ok(Self(value))
22 } else {
23 Err(OperationNameError(value))
24 }
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32
33 #[must_use]
35 pub fn into_string(self) -> String {
36 self.0
37 }
38}
39
40impl fmt::Display for OperationName {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(&self.0)
43 }
44}
45
46impl FromStr for OperationName {
47 type Err = OperationNameError;
48
49 fn from_str(value: &str) -> Result<Self, Self::Err> {
50 Self::new(value)
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56#[error("invalid operation name {0}; expected lowercase dotted segments")]
57pub struct OperationNameError(String);
58
59fn valid_operation_name(value: &str) -> bool {
60 let length = value.chars().count();
61 if !(3..=MAX_OPERATION_NAME_CHARS).contains(&length) || !value.contains('.') {
62 return false;
63 }
64 value.split('.').all(valid_name_segment)
65}
66
67fn valid_name_segment(segment: &str) -> bool {
68 let mut chars = segment.chars();
69 matches!(chars.next(), Some('a'..='z'))
70 && chars.all(|character| {
71 character.is_ascii_lowercase()
72 || character.is_ascii_digit()
73 || matches!(character, '-' | '_')
74 })
75 && !segment.ends_with(['-', '_'])
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81#[serde(rename_all = "snake_case")]
82#[non_exhaustive]
83pub enum TargetKind {
84 Host,
86 DockerDaemon,
88 Container,
90 ComposeProject,
92 IncusServer,
94 IncusInstance,
96 Image,
98 Network,
100 StoragePool,
102 StorageVolume,
104 File,
106 Process,
108 LogSource,
110 ZfsPool,
112 ZfsDataset,
114 ZfsSnapshot,
116 Custom(OperationName),
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123pub struct TargetRef {
124 kind: TargetKind,
125 id: String,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 host: Option<String>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 parent: Option<Box<TargetRef>>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 revision: Option<String>,
132}
133
134impl TargetRef {
135 pub fn new(kind: TargetKind, id: impl Into<String>) -> Result<Self, TargetRefError> {
137 let id = id.into();
138 validate_target_value("id", &id)?;
139 Ok(Self {
140 kind,
141 id,
142 host: None,
143 parent: None,
144 revision: None,
145 })
146 }
147
148 pub fn with_host(mut self, host: impl Into<String>) -> Result<Self, TargetRefError> {
150 let host = host.into();
151 validate_target_value("host", &host)?;
152 self.host = Some(host);
153 Ok(self)
154 }
155
156 pub fn with_parent(mut self, parent: TargetRef) -> Result<Self, TargetRefError> {
158 if parent.depth() >= MAX_TARGET_DEPTH {
159 return Err(TargetRefError::ExcessiveDepth {
160 max_depth: MAX_TARGET_DEPTH,
161 });
162 }
163 self.parent = Some(Box::new(parent));
164 Ok(self)
165 }
166
167 pub fn with_revision(mut self, revision: impl Into<String>) -> Result<Self, TargetRefError> {
169 let revision = revision.into();
170 validate_target_value("revision", &revision)?;
171 self.revision = Some(revision);
172 Ok(self)
173 }
174
175 #[must_use]
177 pub fn kind(&self) -> &TargetKind {
178 &self.kind
179 }
180
181 #[must_use]
183 pub fn id(&self) -> &str {
184 &self.id
185 }
186
187 #[must_use]
189 pub fn host(&self) -> Option<&str> {
190 self.host.as_deref()
191 }
192
193 #[must_use]
195 pub fn parent(&self) -> Option<&TargetRef> {
196 self.parent.as_deref()
197 }
198
199 #[must_use]
201 pub fn revision(&self) -> Option<&str> {
202 self.revision.as_deref()
203 }
204
205 fn depth(&self) -> usize {
206 1 + self.parent.as_deref().map_or(0, Self::depth)
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
212#[non_exhaustive]
213pub enum TargetRefError {
214 #[error("invalid target {field}: expected 1..={max_chars} non-control characters")]
216 InvalidValue {
217 field: &'static str,
219 max_chars: usize,
221 },
222 #[error("target parent chain exceeds maximum depth {max_depth}")]
224 ExcessiveDepth {
225 max_depth: usize,
227 },
228}
229
230fn validate_target_value(field: &'static str, value: &str) -> Result<(), TargetRefError> {
231 let chars = value.chars().count();
232 if chars == 0 || chars > MAX_TARGET_VALUE_CHARS || value.chars().any(char::is_control) {
233 return Err(TargetRefError::InvalidValue {
234 field,
235 max_chars: MAX_TARGET_VALUE_CHARS,
236 });
237 }
238 Ok(())
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
244#[serde(transparent)]
245pub struct IdempotencyKey(String);
246
247impl IdempotencyKey {
248 pub fn new(value: impl Into<String>) -> Result<Self, IdempotencyKeyError> {
250 let value = value.into();
251 let chars = value.chars().count();
252 if chars == 0 || chars > MAX_IDEMPOTENCY_KEY_CHARS || value.chars().any(char::is_control) {
253 return Err(IdempotencyKeyError);
254 }
255 Ok(Self(value))
256 }
257
258 #[must_use]
260 pub fn as_str(&self) -> &str {
261 &self.0
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
267#[error("invalid idempotency key")]
268pub struct IdempotencyKeyError;
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273#[serde(rename_all = "snake_case")]
274#[non_exhaustive]
275pub enum AccessClass {
276 Read,
278 Mutation,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285#[serde(rename_all = "snake_case")]
286#[non_exhaustive]
287pub enum RiskClass {
288 Safe,
290 Disruptive,
292 Destructive,
294 Privileged,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
300#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
301#[serde(rename_all = "snake_case")]
302#[non_exhaustive]
303pub enum Reversibility {
304 Reversible,
306 Conditional,
308 Irreversible,
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
315#[serde(rename_all = "snake_case")]
316#[non_exhaustive]
317pub enum RetryClass {
318 Never,
320 Safe,
322 Conditional,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
329#[serde(rename_all = "snake_case")]
330#[non_exhaustive]
331pub enum MutationSendState {
332 NotSent,
334 Sent,
336 Unknown,
338 NotApplicable,
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
344#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
345#[serde(rename_all = "snake_case")]
346#[non_exhaustive]
347pub enum VerificationStatus {
348 Verified,
350 Failed,
352 Inconclusive,
354 NotSupported,
356 NotRequested,
358}
359
360#[cfg(test)]
361#[path = "model_tests.rs"]
362mod tests;