1use serde::de::{Error as _, SeqAccess, Visitor};
2use serde::{Deserialize, Deserializer, Serialize};
3use std::borrow::Borrow;
4use std::collections::BTreeSet;
5use std::fmt;
6use std::str::FromStr;
7
8use crate::error::{FlowError, Result};
9
10const MAX_WORKFLOW_PATCH_ID_BYTES: usize = 128;
11
12pub const MAX_WORKFLOW_PATCH_MARKERS: usize = 256;
14
15#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[serde(transparent)]
22pub struct WorkflowPatchId(String);
23
24impl WorkflowPatchId {
25 pub fn new(value: impl Into<String>) -> Result<Self> {
27 let value = value.into();
28 validate_workflow_patch_id(&value)?;
29 Ok(Self(value))
30 }
31
32 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36}
37
38impl fmt::Display for WorkflowPatchId {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str(&self.0)
41 }
42}
43
44impl AsRef<str> for WorkflowPatchId {
45 fn as_ref(&self) -> &str {
46 self.as_str()
47 }
48}
49
50impl Borrow<str> for WorkflowPatchId {
51 fn borrow(&self) -> &str {
52 self.as_str()
53 }
54}
55
56impl FromStr for WorkflowPatchId {
57 type Err = FlowError;
58
59 fn from_str(value: &str) -> Result<Self> {
60 Self::new(value)
61 }
62}
63
64impl<'de> Deserialize<'de> for WorkflowPatchId {
65 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
66 where
67 D: Deserializer<'de>,
68 {
69 let value = String::deserialize(deserializer)?;
70 Self::new(value).map_err(D::Error::custom)
71 }
72}
73
74pub(crate) fn deserialize_patch_markers<'de, D>(
75 deserializer: D,
76) -> std::result::Result<BTreeSet<WorkflowPatchId>, D::Error>
77where
78 D: Deserializer<'de>,
79{
80 struct PatchMarkerSetVisitor;
81
82 impl<'de> Visitor<'de> for PatchMarkerSetVisitor {
83 type Value = BTreeSet<WorkflowPatchId>;
84
85 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(
87 formatter,
88 "at most {MAX_WORKFLOW_PATCH_MARKERS} unique workflow patch IDs"
89 )
90 }
91
92 fn visit_seq<A>(
93 self,
94 mut sequence: A,
95 ) -> std::result::Result<BTreeSet<WorkflowPatchId>, A::Error>
96 where
97 A: SeqAccess<'de>,
98 {
99 if sequence
100 .size_hint()
101 .is_some_and(|size| size > MAX_WORKFLOW_PATCH_MARKERS)
102 {
103 return Err(A::Error::custom(format!(
104 "workflow patch marker count exceeds {MAX_WORKFLOW_PATCH_MARKERS}"
105 )));
106 }
107
108 let mut markers = BTreeSet::new();
109 while let Some(marker) = sequence.next_element::<WorkflowPatchId>()? {
110 if markers.len() == MAX_WORKFLOW_PATCH_MARKERS {
111 return Err(A::Error::custom(format!(
112 "workflow patch marker count exceeds {MAX_WORKFLOW_PATCH_MARKERS}"
113 )));
114 }
115 if !markers.insert(marker) {
116 return Err(A::Error::custom(
117 "workflow patch markers contain a duplicate ID",
118 ));
119 }
120 }
121 Ok(markers)
122 }
123 }
124
125 deserializer.deserialize_seq(PatchMarkerSetVisitor)
126}
127
128fn validate_workflow_patch_id(value: &str) -> Result<()> {
129 if value.is_empty() {
130 return Err(FlowError::InvalidWorkflowPatchId(
131 "workflow patch id must not be empty".to_string(),
132 ));
133 }
134 if value.len() > MAX_WORKFLOW_PATCH_ID_BYTES {
135 return Err(FlowError::InvalidWorkflowPatchId(format!(
136 "workflow patch id must not exceed {MAX_WORKFLOW_PATCH_ID_BYTES} bytes"
137 )));
138 }
139 if !value.is_ascii() {
140 return Err(FlowError::InvalidWorkflowPatchId(
141 "workflow patch id must contain only ASCII characters".to_string(),
142 ));
143 }
144 if !value
145 .as_bytes()
146 .first()
147 .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
148 || !value
149 .as_bytes()
150 .last()
151 .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
152 {
153 return Err(FlowError::InvalidWorkflowPatchId(
154 "workflow patch id must start and end with a lowercase ASCII letter or digit"
155 .to_string(),
156 ));
157 }
158 if !value.bytes().all(|byte| {
159 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_' | b'.')
160 }) {
161 return Err(FlowError::InvalidWorkflowPatchId(
162 "workflow patch id contains an unsupported character".to_string(),
163 ));
164 }
165 Ok(())
166}