turnframe_core/operation/
argument.rs1use serde::{Deserialize, Serialize};
5
6use crate::ids::{ReadToolKey, WorkflowKey};
7use crate::locale::Locale;
8use crate::operation::value::DateDirection;
9
10#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
12#[serde(tag = "kind", rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum ArgumentSource {
15 #[default]
17 User,
18 Inferred,
20 Server {
22 read: ReadToolKey,
24 },
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(tag = "kind", rename_all = "snake_case")]
30#[non_exhaustive]
31pub enum ValueShape {
32 Text {
34 written: bool,
36 },
37 Enum {
39 values: Vec<String>,
41 },
42 Integer,
44 Number,
46 Bool,
48 Date {
50 direction: DateDirection,
52 },
53 Money,
55 Record {
57 workflow: WorkflowKey,
59 },
60 Structured,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct ArgumentLabel {
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub locale: Option<Locale>,
70 pub text: String,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[non_exhaustive]
77pub struct ArgumentSpec {
78 pub name: String,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub description: Option<String>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub labels: Vec<ArgumentLabel>,
86 pub required: bool,
88 pub source: ArgumentSource,
90 pub shape: ValueShape,
92 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
95 pub names_the_record: bool,
96}
97
98impl ArgumentSpec {
99 #[must_use]
101 pub fn new(name: impl Into<String>, shape: ValueShape) -> Self {
102 Self {
103 name: name.into(),
104 description: None,
105 labels: Vec::new(),
106 required: false,
107 source: ArgumentSource::User,
108 shape,
109 names_the_record: false,
110 }
111 }
112
113 #[must_use]
115 pub fn label(mut self, text: impl Into<String>) -> Self {
116 self.labels.push(ArgumentLabel {
117 locale: None,
118 text: text.into(),
119 });
120 self
121 }
122
123 #[must_use]
125 pub fn label_in(mut self, locale: impl Into<Locale>, text: impl Into<String>) -> Self {
126 self.labels.push(ArgumentLabel {
127 locale: Some(locale.into()),
128 text: text.into(),
129 });
130 self
131 }
132
133 #[must_use]
135 pub fn describe(mut self, description: impl Into<String>) -> Self {
136 self.description = Some(description.into());
137 self
138 }
139
140 #[must_use]
142 pub const fn required(mut self) -> Self {
143 self.required = true;
144 self
145 }
146
147 #[must_use]
149 pub const fn names_the_record(mut self) -> Self {
150 self.names_the_record = true;
151 self
152 }
153
154 #[must_use]
156 pub const fn optional(mut self) -> Self {
157 self.required = false;
158 self
159 }
160
161 #[must_use]
163 pub fn inferred(mut self) -> Self {
164 self.source = ArgumentSource::Inferred;
165 self
166 }
167
168 #[must_use]
170 pub fn from_read(mut self, read: impl Into<ReadToolKey>) -> Self {
171 self.source = ArgumentSource::Server { read: read.into() };
172 self
173 }
174
175 #[must_use]
177 pub fn written(mut self) -> Self {
178 if let ValueShape::Text { written } = &mut self.shape {
179 *written = true;
180 }
181 self
182 }
183
184 #[must_use]
186 pub fn date_direction(mut self, direction: DateDirection) -> Self {
187 if let ValueShape::Date { direction: current } = &mut self.shape {
188 *current = direction;
189 }
190 self
191 }
192
193 #[must_use]
195 pub fn money(mut self) -> Self {
196 self.shape = ValueShape::Money;
197 self
198 }
199
200 #[must_use]
206 pub fn record(mut self, workflow: impl Into<WorkflowKey>) -> Self {
207 self.shape = ValueShape::Record {
208 workflow: workflow.into(),
209 };
210 self
211 }
212
213 pub fn labels_for<'a>(&'a self, locale: &'a Locale) -> impl Iterator<Item = &'a str> + 'a {
215 let own = self
216 .labels
217 .iter()
218 .filter(move |label| label.locale.as_ref() == Some(locale));
219 let universal = self.labels.iter().filter(|label| label.locale.is_none());
220 own.chain(universal).map(|label| label.text.as_str())
221 }
222}
223
224#[must_use]
228pub fn shape_of(property: &serde_json::Value, defs: Option<&serde_json::Value>) -> ValueShape {
229 if let Some(value) = without_null(property) {
230 return shape_of(&value, defs);
231 }
232 let resolved = resolve_ref(property, defs).unwrap_or(property);
233 if let Some(values) = enum_values(resolved) {
234 return ValueShape::Enum { values };
235 }
236 let kind = resolved.get("type").and_then(serde_json::Value::as_str);
237 match kind {
238 Some("string")
239 if resolved.get("format").and_then(serde_json::Value::as_str) == Some("date") =>
240 {
241 ValueShape::Date {
242 direction: DateDirection::Any,
243 }
244 }
245 Some("string") => ValueShape::Text { written: false },
246 Some("integer") => ValueShape::Integer,
247 Some("number") => ValueShape::Number,
248 Some("boolean") => ValueShape::Bool,
249 _ => ValueShape::Structured,
250 }
251}
252
253fn without_null(property: &serde_json::Value) -> Option<serde_json::Value> {
255 let is_null = |kind: &serde_json::Value| kind.as_str() == Some("null");
256 if let Some(kinds) = property.get("type").and_then(serde_json::Value::as_array) {
257 let kept: Vec<&serde_json::Value> = kinds.iter().filter(|kind| !is_null(kind)).collect();
258 let [only] = kept.as_slice() else {
259 return None;
260 };
261 let mut value = property.clone();
262 value["type"] = (*only).clone();
263 return Some(value);
264 }
265 let branches = property
266 .get("anyOf")
267 .or_else(|| property.get("oneOf"))?
268 .as_array()?;
269 let kept: Vec<serde_json::Value> = branches
270 .iter()
271 .filter(|branch| !branch.get("type").is_some_and(is_null))
272 .cloned()
273 .collect();
274 match kept.as_slice() {
275 _ if kept.len() == branches.len() => None,
276 [only] => Some(only.clone()),
277 _ => Some(serde_json::json!({ "anyOf": kept })),
278 }
279}
280
281fn resolve_ref<'a>(
282 property: &'a serde_json::Value,
283 defs: Option<&'a serde_json::Value>,
284) -> Option<&'a serde_json::Value> {
285 let reference = property.get("$ref")?.as_str()?;
286 let name = reference.rsplit('/').next()?;
287 defs?.get(name)
288}
289
290fn enum_values(schema: &serde_json::Value) -> Option<Vec<String>> {
291 if let Some(values) = schema.get("enum").and_then(serde_json::Value::as_array) {
292 return values
293 .iter()
294 .map(|value| value.as_str().map(str::to_owned))
295 .collect();
296 }
297 let branches = schema
298 .get("oneOf")
299 .or_else(|| schema.get("anyOf"))?
300 .as_array()?;
301 branches
302 .iter()
303 .map(|branch| branch.get("const")?.as_str().map(str::to_owned))
304 .collect()
305}
306
307#[cfg(test)]
308mod tests {
309 use serde_json::json;
310
311 use super::*;
312
313 #[test]
314 fn an_optional_argument_has_the_shape_of_its_value() {
315 let defs = json!({"Kind": {"enum": ["a", "b"]}});
316 let text = ValueShape::Text { written: false };
317 assert_eq!(shape_of(&json!({"type": ["string", "null"]}), None), text);
318 assert_eq!(
319 shape_of(
320 &json!({"anyOf": [{"type": "string"}, {"type": "null"}]}),
321 None
322 ),
323 text
324 );
325 assert_eq!(
326 shape_of(
327 &json!({"anyOf": [{"$ref": "#/$defs/Kind"}, {"type": "null"}]}),
328 Some(&defs)
329 ),
330 ValueShape::Enum {
331 values: vec!["a".into(), "b".into()]
332 }
333 );
334 assert_eq!(
335 shape_of(&json!({"type": ["integer", "null"]}), None),
336 ValueShape::Integer
337 );
338 }
339
340 #[test]
341 fn shapes_follow_the_argument_types_schema() {
342 let defs = json!({"Kind": {"oneOf": [{"type": "string", "const": "a"}, {"type": "string", "const": "b"}]}});
343 assert_eq!(
344 shape_of(&json!({"type": "string"}), None),
345 ValueShape::Text { written: false }
346 );
347 assert_eq!(
348 shape_of(&json!({"type": "string", "format": "date"}), None),
349 ValueShape::Date {
350 direction: DateDirection::Any
351 }
352 );
353 assert_eq!(
354 shape_of(&json!({"$ref": "#/$defs/Kind"}), Some(&defs)),
355 ValueShape::Enum {
356 values: vec!["a".into(), "b".into()]
357 }
358 );
359 assert_eq!(
360 shape_of(&json!({"type": "integer"}), None),
361 ValueShape::Integer
362 );
363 assert_eq!(
364 shape_of(&json!({"type": "object"}), None),
365 ValueShape::Structured
366 );
367 }
368
369 #[test]
370 fn labels_in_the_turns_language_come_first() {
371 let spec = ArgumentSpec::new("value", ValueShape::Text { written: false })
372 .label("subject")
373 .label_in("it-IT", "oggetto");
374 let (it, en) = (Locale::from("it-IT"), Locale::from("en-GB"));
375 let italian: Vec<&str> = spec.labels_for(&it).collect();
376 assert_eq!(italian, vec!["oggetto", "subject"]);
377 let english: Vec<&str> = spec.labels_for(&en).collect();
378 assert_eq!(english, vec!["subject"]);
379 }
380}