Skip to main content

qs_strategy/
value.rs

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/// A scalar type understood by configured expressions.
10#[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/// A scalar expression type with explicit optionality.
24#[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/// A typed runtime value. Missing always retains its scalar type.
49#[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/// A strict configured literal, including an explicitly typed missing value.
95#[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}