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 Ratio,
18 Percent,
19 PricePerObservation,
20 PricePerObservationSquared,
21 RatioPerObservation,
22 RatioPerObservationSquared,
23 LogReturn,
24 LogReturnVariance,
25 Timestamp,
26 Duration,
27 Text,
28 Side,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct ValueType {
35 pub scalar: ScalarType,
36 #[serde(default)]
37 pub optional: bool,
38}
39
40impl ValueType {
41 pub const fn required(scalar: ScalarType) -> Self {
42 Self {
43 scalar,
44 optional: false,
45 }
46 }
47
48 pub const fn optional(scalar: ScalarType) -> Self {
49 Self {
50 scalar,
51 optional: true,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub enum Value {
59 Missing(ScalarType),
60 Bool(bool),
61 Integer(i64),
62 Number(f64),
63 Price(f64),
64 Ratio(f64),
65 Percent(f64),
66 PricePerObservation(f64),
67 PricePerObservationSquared(f64),
68 RatioPerObservation(f64),
69 RatioPerObservationSquared(f64),
70 LogReturn(f64),
71 LogReturnVariance(f64),
72 Timestamp(NaiveDateTime),
73 Duration(Duration),
74 Text(String),
75 Side(Side),
76}
77
78impl Value {
79 pub fn scalar_type(&self) -> ScalarType {
80 match self {
81 Self::Missing(value_type) => *value_type,
82 Self::Bool(_) => ScalarType::Bool,
83 Self::Integer(_) => ScalarType::Integer,
84 Self::Number(_) => ScalarType::Number,
85 Self::Price(_) => ScalarType::Price,
86 Self::Ratio(_) => ScalarType::Ratio,
87 Self::Percent(_) => ScalarType::Percent,
88 Self::PricePerObservation(_) => ScalarType::PricePerObservation,
89 Self::PricePerObservationSquared(_) => ScalarType::PricePerObservationSquared,
90 Self::RatioPerObservation(_) => ScalarType::RatioPerObservation,
91 Self::RatioPerObservationSquared(_) => ScalarType::RatioPerObservationSquared,
92 Self::LogReturn(_) => ScalarType::LogReturn,
93 Self::LogReturnVariance(_) => ScalarType::LogReturnVariance,
94 Self::Timestamp(_) => ScalarType::Timestamp,
95 Self::Duration(_) => ScalarType::Duration,
96 Self::Text(_) => ScalarType::Text,
97 Self::Side(_) => ScalarType::Side,
98 }
99 }
100
101 pub fn is_missing(&self) -> bool {
102 matches!(self, Self::Missing(_))
103 }
104
105 pub(crate) fn finite(self, path: &str) -> Result<Self, crate::EvaluationError> {
106 match self {
107 Self::Number(value)
108 | Self::Price(value)
109 | Self::Ratio(value)
110 | Self::Percent(value)
111 | Self::PricePerObservation(value)
112 | Self::PricePerObservationSquared(value)
113 | Self::RatioPerObservation(value)
114 | Self::RatioPerObservationSquared(value)
115 | Self::LogReturn(value)
116 | Self::LogReturnVariance(value)
117 if !value.is_finite() =>
118 {
119 Err(crate::EvaluationError::NonFinite { path: path.into() })
120 }
121 value => Ok(value),
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128#[serde(
129 tag = "type",
130 content = "value",
131 rename_all = "snake_case",
132 deny_unknown_fields
133)]
134pub enum Literal {
135 Missing(ScalarType),
136 Bool(bool),
137 Integer(i64),
138 Number(f64),
139 Price(f64),
140 Ratio(f64),
141 Percent(f64),
142 PricePerObservation(f64),
143 PricePerObservationSquared(f64),
144 RatioPerObservation(f64),
145 RatioPerObservationSquared(f64),
146 LogReturn(f64),
147 LogReturnVariance(f64),
148 Timestamp(NaiveDateTime),
149 DurationMillis(i64),
150 Text(String),
151 Side(Side),
152}
153
154impl Literal {
155 pub fn value_type(&self) -> ValueType {
156 match self {
157 Self::Missing(value_type) => ValueType::optional(*value_type),
158 Self::Bool(_) => ValueType::required(ScalarType::Bool),
159 Self::Integer(_) => ValueType::required(ScalarType::Integer),
160 Self::Number(_) => ValueType::required(ScalarType::Number),
161 Self::Price(_) => ValueType::required(ScalarType::Price),
162 Self::Ratio(_) => ValueType::required(ScalarType::Ratio),
163 Self::Percent(_) => ValueType::required(ScalarType::Percent),
164 Self::PricePerObservation(_) => ValueType::required(ScalarType::PricePerObservation),
165 Self::PricePerObservationSquared(_) => {
166 ValueType::required(ScalarType::PricePerObservationSquared)
167 }
168 Self::RatioPerObservation(_) => ValueType::required(ScalarType::RatioPerObservation),
169 Self::RatioPerObservationSquared(_) => {
170 ValueType::required(ScalarType::RatioPerObservationSquared)
171 }
172 Self::LogReturn(_) => ValueType::required(ScalarType::LogReturn),
173 Self::LogReturnVariance(_) => ValueType::required(ScalarType::LogReturnVariance),
174 Self::Timestamp(_) => ValueType::required(ScalarType::Timestamp),
175 Self::DurationMillis(_) => ValueType::required(ScalarType::Duration),
176 Self::Text(_) => ValueType::required(ScalarType::Text),
177 Self::Side(_) => ValueType::required(ScalarType::Side),
178 }
179 }
180
181 pub fn to_value(&self) -> Result<Value, crate::CompileError> {
182 let value = match self {
183 Self::Missing(value_type) => Value::Missing(*value_type),
184 Self::Bool(value) => Value::Bool(*value),
185 Self::Integer(value) => Value::Integer(*value),
186 Self::Number(value) => Value::Number(*value),
187 Self::Price(value) => Value::Price(*value),
188 Self::Ratio(value) => Value::Ratio(*value),
189 Self::Percent(value) => Value::Percent(*value),
190 Self::PricePerObservation(value) => Value::PricePerObservation(*value),
191 Self::PricePerObservationSquared(value) => Value::PricePerObservationSquared(*value),
192 Self::RatioPerObservation(value) => Value::RatioPerObservation(*value),
193 Self::RatioPerObservationSquared(value) => Value::RatioPerObservationSquared(*value),
194 Self::LogReturn(value) => Value::LogReturn(*value),
195 Self::LogReturnVariance(value) => Value::LogReturnVariance(*value),
196 Self::Timestamp(value) => Value::Timestamp(*value),
197 Self::DurationMillis(value) => Value::Duration(Duration::milliseconds(*value)),
198 Self::Text(value) => {
199 validate_text(value, MAX_TEXT_BYTES).map_err(|reason| {
200 crate::CompileError::InvalidConfig {
201 path: "literal.text".into(),
202 reason,
203 }
204 })?;
205 Value::Text(value.clone())
206 }
207 Self::Side(value) => Value::Side(*value),
208 };
209 match value {
210 Value::Number(number)
211 | Value::Price(number)
212 | Value::Ratio(number)
213 | Value::Percent(number)
214 | Value::PricePerObservation(number)
215 | Value::PricePerObservationSquared(number)
216 | Value::RatioPerObservation(number)
217 | Value::RatioPerObservationSquared(number)
218 | Value::LogReturn(number)
219 | Value::LogReturnVariance(number)
220 if !number.is_finite() =>
221 {
222 Err(crate::CompileError::InvalidConfig {
223 path: "literal".into(),
224 reason: "number must be finite".into(),
225 })
226 }
227 _ => Ok(value),
228 }
229 }
230}
231
232pub(crate) fn validate_id(value: &str) -> Result<(), String> {
233 if value.is_empty() {
234 return Err("must not be empty".into());
235 }
236 if value.len() > MAX_ID_BYTES {
237 return Err(format!("must not exceed {MAX_ID_BYTES} bytes"));
238 }
239 if !value
240 .bytes()
241 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
242 {
243 return Err("must contain only ASCII letters, digits, '_', '-', or '.'".into());
244 }
245 Ok(())
246}
247
248pub(crate) fn validate_text(value: &str, limit: usize) -> Result<(), String> {
249 if value.is_empty() {
250 return Err("must not be empty".into());
251 }
252 if value != value.trim() {
253 return Err("must not have leading or trailing whitespace".into());
254 }
255 if value.chars().any(char::is_control) {
256 return Err("must not contain control characters".into());
257 }
258 if value.len() > limit {
259 return Err(format!("must not exceed {limit} bytes"));
260 }
261 Ok(())
262}