1pub use crate::model::{InputValue, StudySource};
4
5#[derive(Clone, Debug)]
7#[non_exhaustive]
8pub enum InputKind {
9 Length(usize, usize),
11 Integer(i32, i32),
13 Number(f64, f64),
15 Choice(Vec<String>),
17 Toggle,
19 Source,
21 Color,
23 Timeframe,
25 Symbol,
27 Text,
29}
30
31#[derive(Clone, Debug)]
33pub struct StudyInput {
34 pub id: String,
36 pub label: String,
38 pub value: InputValue,
40 pub default: InputValue,
42 pub kind: InputKind,
44}
45
46impl StudyInput {
47 pub fn new(
49 id: impl Into<String>,
50 label: impl Into<String>,
51 value: InputValue,
52 kind: InputKind,
53 ) -> Self {
54 Self {
55 id: id.into(),
56 label: label.into(),
57 default: value.clone(),
58 value,
59 kind,
60 }
61 }
62
63 pub fn with_default(mut self, value: InputValue) -> Self {
65 self.default = value;
66 self
67 }
68
69 pub fn accepts(&self, value: &InputValue) -> bool {
71 match (&self.kind, value) {
72 (InputKind::Length(min, max), InputValue::Length(v)) => (*min..=*max).contains(v),
73 (InputKind::Integer(min, max), InputValue::Integer(v)) => (*min..=*max).contains(v),
74 (InputKind::Number(min, max), InputValue::Number(v)) => {
75 v.is_finite() && (*min..=*max).contains(v)
76 }
77 (InputKind::Choice(choices), InputValue::Choice(v)) => *v < choices.len(),
78 (InputKind::Toggle, InputValue::Toggle(_))
79 | (InputKind::Source, InputValue::Source(_))
80 | (InputKind::Color, InputValue::Color(_))
81 | (InputKind::Timeframe, InputValue::Timeframe(_)) => true,
82 (InputKind::Symbol, InputValue::Symbol(value))
83 | (InputKind::Text, InputValue::Text(value)) => {
84 value.trim() == value && !value.chars().any(char::is_control)
85 }
86 _ => false,
87 }
88 }
89}
90
91#[cfg(feature = "studies")]
92macro_rules! input_spec {
93 ($id:expr, $value:expr, integer($label:expr, $min:expr, $max:expr)) => {
94 crate::studies::StudyInput::new(
95 $id,
96 $label,
97 crate::studies::InputValue::Integer($value),
98 crate::studies::InputKind::Integer($min, $max),
99 )
100 };
101 ($id:expr, $value:expr, length($label:expr, $min:expr, $max:expr)) => {
102 crate::studies::StudyInput::new(
103 $id,
104 $label,
105 crate::studies::InputValue::Length($value),
106 crate::studies::InputKind::Length($min, $max),
107 )
108 };
109 ($id:expr, $value:expr, number($label:expr, $min:expr, $max:expr)) => {
110 crate::studies::StudyInput::new(
111 $id,
112 $label,
113 crate::studies::InputValue::Number($value as f64),
114 crate::studies::InputKind::Number($min, $max),
115 )
116 };
117 ($id:expr, $value:expr, choice($label:expr, $choices:expr)) => {{
118 let choices = $choices;
119 crate::studies::StudyInput::new(
120 $id,
121 $label,
122 crate::studies::InputValue::Choice(
123 choices.iter().position(|v| *v == $value).unwrap_or(0),
124 ),
125 crate::studies::InputKind::Choice(choices.iter().map(|v| format!("{v:?}")).collect()),
126 )
127 }};
128 ($id:expr, $value:expr, toggle($label:expr)) => {
129 crate::studies::StudyInput::new(
130 $id,
131 $label,
132 crate::studies::InputValue::Toggle($value),
133 crate::studies::InputKind::Toggle,
134 )
135 };
136 ($id:expr, $value:expr, color($label:expr)) => {
137 crate::studies::StudyInput::new(
138 $id,
139 $label,
140 crate::studies::InputValue::Color($value),
141 crate::studies::InputKind::Color,
142 )
143 };
144}
145
146#[cfg(feature = "studies")]
147macro_rules! input_assign {
148 ($value:expr, integer($($args:expr),*)) => {
149 match $value {
150 crate::studies::InputValue::Integer(v) => v,
151 _ => unreachable!(),
152 }
153 };
154 ($value:expr, length($($args:expr),*)) => {
155 match $value {
156 crate::studies::InputValue::Length(v) => v,
157 _ => unreachable!(),
158 }
159 };
160 ($value:expr, number($($args:expr),*)) => {
161 match $value {
162 crate::studies::InputValue::Number(v) => v as _,
163 _ => unreachable!(),
164 }
165 };
166 ($value:expr, choice($label:expr, $choices:expr)) => {
167 match $value {
168 crate::studies::InputValue::Choice(v) => $choices
169 .into_iter()
170 .nth(v)
171 .expect("validated study input choice"),
172 _ => unreachable!(),
173 }
174 };
175 ($value:expr, toggle($label:expr)) => {
176 match $value {
177 crate::studies::InputValue::Toggle(v) => v,
178 _ => unreachable!(),
179 }
180 };
181 ($value:expr, color($label:expr)) => {
182 match $value {
183 crate::studies::InputValue::Color(v) => v,
184 _ => unreachable!(),
185 }
186 };
187}
188
189#[cfg(feature = "studies")]
190macro_rules! study_inputs {
191 ($this:ident; $($field:ident $([$index:literal])? : $kind:ident($($args:expr),*)),+ $(,)? ; $($changed:ident =>)? $after:block) => {
192 fn inputs(&$this) -> Vec<crate::studies::StudyInput> {
193 let defaults = Self::default();
194 vec![$(crate::studies::inputs::input_spec!(stringify!($field $([$index])?), $this.$field $([$index])?, $kind($($args),*))
195 .with_default(crate::studies::inputs::input_spec!(stringify!($field $([$index])?), defaults.$field $([$index])?, $kind($($args),*)).value)),+]
196 }
197
198 fn set_input(&mut $this, id: &str, value: crate::studies::InputValue) -> Result<(), crate::studies::StudyError> {
199 let schema = $this.inputs().into_iter().find(|input| input.id == id).ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
200 if !schema.accepts(&value) { return Err(crate::studies::StudyError::invalid_input(id)); }
201 match id {
202 $(stringify!($field $([$index])?) => { $this.$field $([$index])? = crate::studies::inputs::input_assign!(value, $kind($($args),*)); }),+
203 _ => return Err(crate::studies::StudyError::UnknownInput(id.into())),
204 }
205 $(let $changed = id;)?
206 $after
207 $this.calculate(&[]);
208 Ok(())
209 }
210 };
211}
212
213#[cfg(feature = "studies")]
214pub(crate) use {input_assign, input_spec, study_inputs};