1use chrono::{Duration, NaiveDateTime};
2use qs_core::Side;
3use serde::{Deserialize, Serialize};
4
5pub const MAX_ID_BYTES: usize = 64;
6pub const MAX_TEXT_BYTES: usize = 256;
7pub const MAX_GENERATED_ID_BYTES: usize = 192;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ScalarType {
13 Bool,
14 Integer,
15 Number,
16 Price,
17 Timestamp,
18 Duration,
19 Text,
20 Side,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct ValueType {
27 pub scalar: ScalarType,
28 #[serde(default)]
29 pub optional: bool,
30}
31
32impl ValueType {
33 pub const fn required(scalar: ScalarType) -> Self {
34 Self {
35 scalar,
36 optional: false,
37 }
38 }
39
40 pub const fn optional(scalar: ScalarType) -> Self {
41 Self {
42 scalar,
43 optional: true,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub enum Value {
51 Missing(ScalarType),
52 Bool(bool),
53 Integer(i64),
54 Number(f64),
55 Price(f64),
56 Timestamp(NaiveDateTime),
57 Duration(Duration),
58 Text(String),
59 Side(Side),
60}
61
62impl Value {
63 pub fn scalar_type(&self) -> ScalarType {
64 match self {
65 Self::Missing(value_type) => *value_type,
66 Self::Bool(_) => ScalarType::Bool,
67 Self::Integer(_) => ScalarType::Integer,
68 Self::Number(_) => ScalarType::Number,
69 Self::Price(_) => ScalarType::Price,
70 Self::Timestamp(_) => ScalarType::Timestamp,
71 Self::Duration(_) => ScalarType::Duration,
72 Self::Text(_) => ScalarType::Text,
73 Self::Side(_) => ScalarType::Side,
74 }
75 }
76
77 pub fn is_missing(&self) -> bool {
78 matches!(self, Self::Missing(_))
79 }
80
81 pub(crate) fn finite(self, path: &str) -> Result<Self, crate::EvaluationError> {
82 match self {
83 Self::Number(value) if !value.is_finite() => {
84 Err(crate::EvaluationError::NonFinite { path: path.into() })
85 }
86 Self::Price(value) if !value.is_finite() => {
87 Err(crate::EvaluationError::NonFinite { path: path.into() })
88 }
89 value => Ok(value),
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(
97 tag = "type",
98 content = "value",
99 rename_all = "snake_case",
100 deny_unknown_fields
101)]
102pub enum Literal {
103 Missing(ScalarType),
104 Bool(bool),
105 Integer(i64),
106 Number(f64),
107 Price(f64),
108 Timestamp(NaiveDateTime),
109 DurationMillis(i64),
110 Text(String),
111 Side(Side),
112}
113
114impl Literal {
115 pub fn value_type(&self) -> ValueType {
116 match self {
117 Self::Missing(value_type) => ValueType::optional(*value_type),
118 Self::Bool(_) => ValueType::required(ScalarType::Bool),
119 Self::Integer(_) => ValueType::required(ScalarType::Integer),
120 Self::Number(_) => ValueType::required(ScalarType::Number),
121 Self::Price(_) => ValueType::required(ScalarType::Price),
122 Self::Timestamp(_) => ValueType::required(ScalarType::Timestamp),
123 Self::DurationMillis(_) => ValueType::required(ScalarType::Duration),
124 Self::Text(_) => ValueType::required(ScalarType::Text),
125 Self::Side(_) => ValueType::required(ScalarType::Side),
126 }
127 }
128
129 pub fn to_value(&self) -> Result<Value, crate::CompileError> {
130 let value = match self {
131 Self::Missing(value_type) => Value::Missing(*value_type),
132 Self::Bool(value) => Value::Bool(*value),
133 Self::Integer(value) => Value::Integer(*value),
134 Self::Number(value) => Value::Number(*value),
135 Self::Price(value) => Value::Price(*value),
136 Self::Timestamp(value) => Value::Timestamp(*value),
137 Self::DurationMillis(value) => Value::Duration(Duration::milliseconds(*value)),
138 Self::Text(value) => {
139 validate_text(value, MAX_TEXT_BYTES).map_err(|reason| {
140 crate::CompileError::InvalidConfig {
141 path: "literal.text".into(),
142 reason,
143 }
144 })?;
145 Value::Text(value.clone())
146 }
147 Self::Side(value) => Value::Side(*value),
148 };
149 match value {
150 Value::Number(number) | Value::Price(number) if !number.is_finite() => {
151 Err(crate::CompileError::InvalidConfig {
152 path: "literal".into(),
153 reason: "number must be finite".into(),
154 })
155 }
156 _ => Ok(value),
157 }
158 }
159}
160
161pub(crate) fn validate_id(value: &str) -> Result<(), String> {
162 if value.is_empty() {
163 return Err("must not be empty".into());
164 }
165 if value.len() > MAX_ID_BYTES {
166 return Err(format!("must not exceed {MAX_ID_BYTES} bytes"));
167 }
168 if !value
169 .bytes()
170 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
171 {
172 return Err("must contain only ASCII letters, digits, '_', '-', or '.'".into());
173 }
174 Ok(())
175}
176
177pub(crate) fn validate_text(value: &str, limit: usize) -> Result<(), String> {
178 if value.is_empty() {
179 return Err("must not be empty".into());
180 }
181 if value != value.trim() {
182 return Err("must not have leading or trailing whitespace".into());
183 }
184 if value.chars().any(char::is_control) {
185 return Err("must not contain control characters".into());
186 }
187 if value.len() > limit {
188 return Err(format!("must not exceed {limit} bytes"));
189 }
190 Ok(())
191}