1use std::collections::BTreeMap;
14
15use rust_decimal::Decimal;
16use serde::{Deserialize, Serialize};
17
18use crate::{ErrorCode, Expression, ExpressionBudget, FileMakerError, Result};
19
20#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct CurrencyValue {
24 pub code: String,
26 #[serde(with = "rust_decimal::serde::str")]
28 pub amount: Decimal,
29}
30
31#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type", content = "value", rename_all = "snake_case")]
34pub enum DataValue {
35 String(String),
37 Integer(i64),
39 Decimal(#[serde(with = "rust_decimal::serde::str")] Decimal),
41 Boolean(bool),
43 Date(String),
45 DateTime(String),
47 Duration(i64),
49 Currency(CurrencyValue),
51 Array(Vec<Self>),
53 Object(BTreeMap<String, Self>),
55 Null,
57}
58
59impl DataValue {
60 #[must_use]
62 pub fn get_path(&self, path: &str) -> Option<&Self> {
63 if path.is_empty() {
64 return Some(self);
65 }
66 let mut value = self;
67 for part in path.split('.') {
68 value = match value {
69 Self::Object(object) => object.get(part)?,
70 Self::Array(array) => array.get(part.parse::<usize>().ok()?)?,
71 _ => return None,
72 };
73 }
74 Some(value)
75 }
76
77 #[must_use]
79 pub fn display(&self) -> String {
80 match self {
81 Self::String(value) | Self::Date(value) | Self::DateTime(value) => value.clone(),
82 Self::Integer(value) | Self::Duration(value) => value.to_string(),
83 Self::Decimal(value) => value.normalize().to_string(),
84 Self::Boolean(value) => value.to_string(),
85 Self::Currency(value) => format!("{} {}", value.amount.normalize(), value.code),
86 Self::Array(_) => "[array]".to_owned(),
87 Self::Object(_) => "[object]".to_owned(),
88 Self::Null => String::new(),
89 }
90 }
91
92 #[must_use]
94 pub fn is_truthy(&self) -> bool {
95 match self {
96 Self::Boolean(value) => *value,
97 Self::Null => false,
98 Self::String(value) => !value.is_empty(),
99 Self::Integer(value) | Self::Duration(value) => *value != 0,
100 Self::Decimal(value) => !value.is_zero(),
101 Self::Currency(value) => !value.amount.is_zero(),
102 Self::Array(value) => !value.is_empty(),
103 Self::Object(value) => !value.is_empty(),
104 Self::Date(_) | Self::DateTime(_) => true,
105 }
106 }
107
108 pub fn validate(
110 &self,
111 max_depth: usize,
112 max_items: usize,
113 max_text_bytes: usize,
114 ) -> Result<()> {
115 let mut stack = vec![(self, 0_usize)];
116 let mut count = 0_usize;
117 while let Some((value, depth)) = stack.pop() {
118 if depth > max_depth {
119 return Err(data_error("data nesting exceeds configured depth"));
120 }
121 count = count.saturating_add(1);
122 if count > max_items {
123 return Err(data_error("data item count exceeds configured limit"));
124 }
125 match value {
126 Self::String(value) => {
127 if value.len() > max_text_bytes {
128 return Err(data_error("data string exceeds configured byte limit"));
129 }
130 }
131 Self::Date(value) => {
132 if value.len() > max_text_bytes || !valid_date(value) {
133 return Err(data_error("date must use a valid YYYY-MM-DD value"));
134 }
135 }
136 Self::DateTime(value) => {
137 if value.len() > max_text_bytes || !valid_date_time(value) {
138 return Err(data_error("date-time must use a valid RFC-3339-like value"));
139 }
140 }
141 Self::Currency(value) if !valid_currency_code(&value.code) => {
142 return Err(data_error(
143 "currency code must be three uppercase ASCII letters",
144 ));
145 }
146 Self::Array(values) => {
147 stack.extend(values.iter().rev().map(|item| (item, depth + 1)));
148 }
149 Self::Object(values) => {
150 if values.keys().any(|key| key.is_empty() || key.len() > 128) {
151 return Err(data_error("object key is empty or too long"));
152 }
153 stack.extend(values.values().rev().map(|item| (item, depth + 1)));
154 }
155 _ => {}
156 }
157 }
158 Ok(())
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub enum DataType {
166 String,
168 Integer,
170 Decimal,
172 Boolean,
174 Date,
176 DateTime,
178 Duration,
180 Currency,
182 Array,
184 Object,
186 Null,
188}
189
190#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
192pub struct DataField {
193 pub data_type: DataType,
195 pub nullable: bool,
197 pub computed: Option<String>,
199}
200
201pub type DataSchema = BTreeMap<String, DataField>;
203
204pub fn resolve_computed_fields(
209 schema: &DataSchema,
210 data: &DataValue,
211 max_expression_steps: usize,
212) -> Result<DataValue> {
213 let DataValue::Object(input) = data else {
214 return Err(data_error("computed schema root requires an object"));
215 };
216 let mut values = input.clone();
217 let mut resolved = std::collections::BTreeSet::new();
218 let mut visiting = std::collections::BTreeSet::new();
219 for name in schema.keys() {
220 resolve_computed_field(
221 name,
222 schema,
223 &mut values,
224 &mut resolved,
225 &mut visiting,
226 max_expression_steps,
227 )?;
228 }
229 let result = DataValue::Object(values);
230 validate_schema(schema, &result)?;
231 Ok(result)
232}
233
234fn resolve_computed_field(
235 name: &str,
236 schema: &DataSchema,
237 values: &mut BTreeMap<String, DataValue>,
238 resolved: &mut std::collections::BTreeSet<String>,
239 visiting: &mut std::collections::BTreeSet<String>,
240 max_expression_steps: usize,
241) -> Result<()> {
242 if resolved.contains(name) {
243 return Ok(());
244 }
245 let Some(field) = schema.get(name) else {
246 return Ok(());
247 };
248 let Some(source) = &field.computed else {
249 resolved.insert(name.to_owned());
250 return Ok(());
251 };
252 if !visiting.insert(name.to_owned()) {
253 return Err(FileMakerError::new(
254 ErrorCode::DataCycle,
255 format!("computed data cycle includes `{name}`"),
256 ));
257 }
258 let expression = Expression::parse(source.clone())?;
259 for dependency in expression.dependencies() {
260 if schema
261 .get(&dependency)
262 .is_some_and(|field| field.computed.is_some())
263 {
264 resolve_computed_field(
265 &dependency,
266 schema,
267 values,
268 resolved,
269 visiting,
270 max_expression_steps,
271 )?;
272 }
273 }
274 let root = DataValue::Object(values.clone());
275 let value = expression.evaluate(&root, &mut ExpressionBudget::new(max_expression_steps)?)?;
276 values.insert(name.to_owned(), value);
277 visiting.remove(name);
278 resolved.insert(name.to_owned());
279 Ok(())
280}
281
282pub fn validate_schema(schema: &DataSchema, data: &DataValue) -> Result<()> {
284 let DataValue::Object(object) = data else {
285 return Err(data_error("schema root requires an object"));
286 };
287 for (name, field) in schema {
288 let Some(value) = object.get(name) else {
289 if field.nullable || field.computed.is_some() {
290 continue;
291 }
292 return Err(data_error(format!(
293 "required data field `{name}` is missing"
294 )));
295 };
296 if matches!(value, DataValue::Null) && field.nullable {
297 continue;
298 }
299 if !matches_type(value, field.data_type) {
300 return Err(data_error(format!(
301 "data field `{name}` has the wrong type"
302 )));
303 }
304 }
305 Ok(())
306}
307
308fn matches_type(value: &DataValue, expected: DataType) -> bool {
309 matches!(
310 (value, expected),
311 (DataValue::String(_), DataType::String)
312 | (DataValue::Integer(_), DataType::Integer)
313 | (DataValue::Decimal(_), DataType::Decimal)
314 | (DataValue::Boolean(_), DataType::Boolean)
315 | (DataValue::Date(_), DataType::Date)
316 | (DataValue::DateTime(_), DataType::DateTime)
317 | (DataValue::Duration(_), DataType::Duration)
318 | (DataValue::Currency(_), DataType::Currency)
319 | (DataValue::Array(_), DataType::Array)
320 | (DataValue::Object(_), DataType::Object)
321 | (DataValue::Null, DataType::Null)
322 )
323}
324
325fn valid_currency_code(code: &str) -> bool {
326 code.len() == 3 && code.bytes().all(|byte| byte.is_ascii_uppercase())
327}
328
329fn valid_date(value: &str) -> bool {
330 if !value.is_ascii() || value.len() != 10 || &value[4..5] != "-" || &value[7..8] != "-" {
331 return false;
332 }
333 let Some(year) = decimal(&value[0..4]) else {
334 return false;
335 };
336 let Some(month) = decimal(&value[5..7]) else {
337 return false;
338 };
339 let Some(day) = decimal(&value[8..10]) else {
340 return false;
341 };
342 let maximum = match month {
343 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
344 4 | 6 | 9 | 11 => 30,
345 2 if leap_year(year) => 29,
346 2 => 28,
347 _ => return false,
348 };
349 (1..=maximum).contains(&day)
350}
351
352fn valid_date_time(value: &str) -> bool {
353 if !value.is_ascii() {
354 return false;
355 }
356 let Some((date, time_and_zone)) = value.split_once('T') else {
357 return false;
358 };
359 if !valid_date(date) {
360 return false;
361 }
362 let (time, zone) = if let Some(time) = time_and_zone.strip_suffix('Z') {
363 (time, None)
364 } else {
365 let Some(index) = time_and_zone
366 .char_indices()
367 .rev()
368 .find_map(|(index, character)| matches!(character, '+' | '-').then_some(index))
369 else {
370 return false;
371 };
372 (&time_and_zone[..index], Some(&time_and_zone[index + 1..]))
373 };
374 let mut parts = time.split(':');
375 let (Some(hour), Some(minute), Some(second), None) =
376 (parts.next(), parts.next(), parts.next(), parts.next())
377 else {
378 return false;
379 };
380 let second = second.split_once('.').map_or(second, |(whole, fraction)| {
381 if fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
382 "invalid"
383 } else {
384 whole
385 }
386 });
387 let valid_time = decimal(hour).is_some_and(|value| value <= 23)
388 && decimal(minute).is_some_and(|value| value <= 59)
389 && decimal(second).is_some_and(|value| value <= 60);
390 valid_time && zone.is_none_or(valid_zone)
391}
392
393fn valid_zone(value: &str) -> bool {
394 value.is_ascii()
395 && value.len() == 5
396 && &value[2..3] == ":"
397 && decimal(&value[..2]).is_some_and(|hour| hour <= 23)
398 && decimal(&value[3..]).is_some_and(|minute| minute <= 59)
399}
400
401fn decimal(value: &str) -> Option<u32> {
402 (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
403 .then(|| value.parse().ok())
404 .flatten()
405}
406
407const fn leap_year(year: u32) -> bool {
408 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
409}
410
411fn data_error(message: impl Into<String>) -> FileMakerError {
412 FileMakerError::new(ErrorCode::DataType, message)
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn resolves_computed_fields_in_dependency_order() {
421 let schema = BTreeMap::from([
422 (
423 "label".to_owned(),
424 DataField {
425 data_type: DataType::String,
426 nullable: false,
427 computed: Some("data.name + data.suffix".to_owned()),
428 },
429 ),
430 (
431 "suffix".to_owned(),
432 DataField {
433 data_type: DataType::String,
434 nullable: false,
435 computed: Some("\"!\"".to_owned()),
436 },
437 ),
438 (
439 "name".to_owned(),
440 DataField {
441 data_type: DataType::String,
442 nullable: false,
443 computed: None,
444 },
445 ),
446 ]);
447 let input = DataValue::Object(BTreeMap::from([(
448 "name".to_owned(),
449 DataValue::String("Ada".to_owned()),
450 )]));
451 let output = resolve_computed_fields(&schema, &input, 32).unwrap();
452 assert_eq!(
453 output.get_path("label"),
454 Some(&DataValue::String("Ada!".to_owned()))
455 );
456 }
457
458 #[test]
459 fn rejects_computed_field_cycles() {
460 let schema = BTreeMap::from([
461 (
462 "a".to_owned(),
463 DataField {
464 data_type: DataType::String,
465 nullable: false,
466 computed: Some("b".to_owned()),
467 },
468 ),
469 (
470 "b".to_owned(),
471 DataField {
472 data_type: DataType::String,
473 nullable: false,
474 computed: Some("a".to_owned()),
475 },
476 ),
477 ]);
478 let error =
479 resolve_computed_fields(&schema, &DataValue::Object(BTreeMap::new()), 8).unwrap_err();
480 assert_eq!(error.code(), ErrorCode::DataCycle);
481 }
482
483 #[test]
484 fn validates_date_and_date_time_values_without_locale_rules() {
485 DataValue::Array(vec![
486 DataValue::Date("2024-02-29".to_owned()),
487 DataValue::DateTime("2026-08-30T12:34:56.123+02:00".to_owned()),
488 DataValue::DateTime("2026-08-30T10:34:56Z".to_owned()),
489 ])
490 .validate(4, 8, 64)
491 .unwrap();
492 assert!(DataValue::Date("2023-02-29".to_owned())
493 .validate(1, 2, 64)
494 .is_err());
495 assert!(DataValue::DateTime("2026-08-30 10:34:56".to_owned())
496 .validate(1, 2, 64)
497 .is_err());
498 }
499}