minco_contract/
request.rs1use std::{collections::BTreeMap, fmt::Write as _};
2
3use serde::{Deserialize, Deserializer};
4
5pub const CONTRACT_VALIDATION_MAX_FIELD_PATHS: usize = 32;
7pub const CONTRACT_VALIDATION_MAX_MESSAGES_PER_PATH: usize = 4;
9pub const CONTRACT_VALIDATION_MAX_PATH_BYTES: usize = 256;
11pub const CONTRACT_VALIDATION_MAX_MESSAGE_BYTES: usize = 256;
13pub const CONTRACT_VALIDATION_MAX_PATH_DEPTH: usize = 16;
15
16const TRUNCATED_PATH: &str = "$._truncated";
17const TRUNCATED_MESSAGE: &str = "additional validation errors omitted";
18const SAFE_FALLBACK_MESSAGE: &str = "validation rule failed";
19
20pub trait ContractValidate {
22 fn validate_contract(&self, errors: &mut ContractValidationErrors);
24}
25
26pub fn deserialize_optional_non_null<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
31where
32 D: Deserializer<'de>,
33 T: Deserialize<'de>,
34{
35 T::deserialize(deserializer).map(Some)
36}
37
38pub fn deserialize_required_nullable<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
43where
44 D: Deserializer<'de>,
45 T: Deserialize<'de>,
46{
47 Option::<T>::deserialize(deserializer)
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51enum PathSegment {
52 Field(&'static str),
53 Index(usize),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ContractValidationErrors {
62 fields: BTreeMap<String, Vec<String>>,
63 path: [Option<PathSegment>; CONTRACT_VALIDATION_MAX_PATH_DEPTH],
64 depth: usize,
65 overflow_depth: usize,
66 truncated: bool,
67}
68
69impl ContractValidationErrors {
70 #[must_use]
72 pub const fn new() -> Self {
73 Self {
74 fields: BTreeMap::new(),
75 path: [None; CONTRACT_VALIDATION_MAX_PATH_DEPTH],
76 depth: 0,
77 overflow_depth: 0,
78 truncated: false,
79 }
80 }
81
82 pub fn at_field(&mut self, field: &'static str, validate: impl FnOnce(&mut Self)) {
84 self.at_segment(PathSegment::Field(field), validate);
85 }
86
87 pub fn at_index(&mut self, index: usize, validate: impl FnOnce(&mut Self)) {
89 self.at_segment(PathSegment::Index(index), validate);
90 }
91
92 pub fn add(&mut self, message: &'static str) {
94 if self.truncated {
95 return;
96 }
97 if self.overflow_depth > 0 {
98 self.mark_truncated();
99 return;
100 }
101 let Some(path) = self.materialize_path() else {
102 self.mark_truncated();
103 return;
104 };
105 let message = if message.len() <= CONTRACT_VALIDATION_MAX_MESSAGE_BYTES {
106 message
107 } else {
108 SAFE_FALLBACK_MESSAGE
109 };
110
111 if let Some(messages) = self.fields.get_mut(&path) {
112 if messages.len() < CONTRACT_VALIDATION_MAX_MESSAGES_PER_PATH {
113 messages.push(message.to_owned());
114 } else {
115 self.mark_truncated();
116 }
117 return;
118 }
119
120 if self.fields.len() >= CONTRACT_VALIDATION_MAX_FIELD_PATHS.saturating_sub(1) {
122 self.mark_truncated();
123 return;
124 }
125 self.fields.insert(path, vec![message.to_owned()]);
126 }
127
128 #[must_use]
130 pub const fn fields(&self) -> &BTreeMap<String, Vec<String>> {
131 &self.fields
132 }
133
134 #[must_use]
136 pub fn into_fields(self) -> BTreeMap<String, Vec<String>> {
137 self.fields
138 }
139
140 #[must_use]
142 pub fn len(&self) -> usize {
143 self.fields.len()
144 }
145
146 #[must_use]
148 pub fn is_empty(&self) -> bool {
149 self.fields.is_empty()
150 }
151
152 #[must_use]
154 pub const fn is_truncated(&self) -> bool {
155 self.truncated
156 }
157
158 fn at_segment(&mut self, segment: PathSegment, validate: impl FnOnce(&mut Self)) {
159 if self.truncated {
160 return;
161 }
162 if self.depth == CONTRACT_VALIDATION_MAX_PATH_DEPTH {
163 self.overflow_depth += 1;
164 validate(self);
165 self.overflow_depth -= 1;
166 return;
167 }
168 self.path[self.depth] = Some(segment);
169 self.depth += 1;
170 validate(self);
171 self.depth -= 1;
172 self.path[self.depth] = None;
173 }
174
175 fn materialize_path(&self) -> Option<String> {
176 if self.depth == 0 {
177 return Some("$".to_owned());
178 }
179 let mut output = String::new();
180 for (position, segment) in self.path[..self.depth].iter().flatten().enumerate() {
181 if position > 0 {
182 output.push('.');
183 }
184 match segment {
185 PathSegment::Field(field) => output.push_str(field),
186 PathSegment::Index(index) => {
187 write!(output, "{index}").expect("writing to String cannot fail");
188 }
189 }
190 if output.len() > CONTRACT_VALIDATION_MAX_PATH_BYTES {
191 return None;
192 }
193 }
194 Some(output)
195 }
196
197 fn mark_truncated(&mut self) {
198 if self.truncated {
199 return;
200 }
201 self.truncated = true;
202 self.fields.insert(
203 TRUNCATED_PATH.to_owned(),
204 vec![TRUNCATED_MESSAGE.to_owned()],
205 );
206 }
207}
208
209impl Default for ContractValidationErrors {
210 fn default() -> Self {
211 Self::new()
212 }
213}