1mod argument;
11mod value;
12
13use std::collections::BTreeMap;
14use std::fmt;
15use std::sync::Arc;
16
17use schemars::{JsonSchema, Schema};
18use serde::de::DeserializeOwned;
19use serde::{Deserialize, Serialize};
20
21use crate::ids::{OperationKey, ReadToolKey, WorkflowKey};
22use crate::locale::Locale;
23use crate::plan::{ActAvailability, ActMutability, TargetPolicy};
24
25pub use argument::{ArgumentLabel, ArgumentSource, ArgumentSpec, ValueShape, shape_of};
26pub use value::{
27 DateDirection, DateError, DateExpr, DatePeriod, DateUnit, DayOfWeek, Money, MoneyError,
28 PeriodOccurrence, WeekdayOccurrence,
29};
30
31type Validator = Arc<dyn Fn(&serde_json::Value) -> Result<(), String> + Send + Sync>;
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct GlossaryTerm {
36 pub term: String,
38 pub meaning: String,
40}
41
42impl GlossaryTerm {
43 #[must_use]
45 pub fn new(term: impl Into<String>, meaning: impl Into<String>) -> Self {
46 Self {
47 term: term.into(),
48 meaning: meaning.into(),
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[non_exhaustive]
56pub struct OperationExample {
57 pub message: String,
59 #[serde(default)]
61 pub arguments: serde_json::Map<String, serde_json::Value>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 pub not_given: Vec<String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
69#[error("operation {operation}: {reason}")]
70pub struct OperationSpecError {
71 pub operation: OperationKey,
73 pub reason: String,
75}
76
77#[derive(Clone, Serialize, Deserialize)]
79#[non_exhaustive]
80pub struct OperationSpec {
81 pub key: OperationKey,
83 pub workflow: WorkflowKey,
85 pub summary: String,
87 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
89 pub summaries: BTreeMap<Locale, String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub guidance: Option<String>,
93 pub target_policy: TargetPolicy,
95 pub mutability: ActMutability,
97 pub availability: ActAvailability,
99 pub arguments_schema: Schema,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub arguments: Vec<ArgumentSpec>,
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
106 pub examples: Vec<OperationExample>,
107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub context_reads: Vec<ReadToolKey>,
110 #[serde(skip)]
111 validator: Option<Validator>,
112 #[serde(skip)]
113 problems: Vec<String>,
114}
115
116impl fmt::Debug for OperationSpec {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.debug_struct("OperationSpec")
119 .field("key", &self.key)
120 .field("workflow", &self.workflow)
121 .field("summary", &self.summary)
122 .field("target_policy", &self.target_policy)
123 .field("arguments", &self.arguments)
124 .field("examples", &self.examples.len())
125 .finish_non_exhaustive()
126 }
127}
128
129impl PartialEq for OperationSpec {
130 fn eq(&self, other: &Self) -> bool {
131 self.key == other.key
132 && self.workflow == other.workflow
133 && self.summary == other.summary
134 && self.summaries == other.summaries
135 && self.guidance == other.guidance
136 && self.target_policy == other.target_policy
137 && self.mutability == other.mutability
138 && self.availability == other.availability
139 && self.arguments_schema == other.arguments_schema
140 && self.arguments == other.arguments
141 && self.examples == other.examples
142 && self.context_reads == other.context_reads
143 }
144}
145
146impl OperationSpec {
147 #[must_use]
149 pub fn new(key: impl Into<OperationKey>) -> Self {
150 Self {
151 key: key.into(),
152 workflow: WorkflowKey::from(""),
153 summary: String::new(),
154 summaries: BTreeMap::new(),
155 guidance: None,
156 target_policy: TargetPolicy::RequiresExistingCase,
157 mutability: ActMutability::Mutating,
158 availability: ActAvailability::Proposable,
159 arguments_schema: schemars::schema_for!(()),
160 arguments: Vec::new(),
161 examples: Vec::new(),
162 context_reads: Vec::new(),
163 validator: None,
164 problems: Vec::new(),
165 }
166 }
167
168 #[must_use]
170 pub fn summary(mut self, summary: impl Into<String>) -> Self {
171 self.summary = summary.into();
172 self
173 }
174
175 #[must_use]
177 pub fn summary_in(mut self, locale: impl Into<Locale>, summary: impl Into<String>) -> Self {
178 self.summaries.insert(locale.into(), summary.into());
179 self
180 }
181
182 #[must_use]
185 pub fn summary_for(&self, locale: &Locale) -> &str {
186 self.summaries
187 .get(locale)
188 .or_else(|| {
189 self.summaries
190 .iter()
191 .find(|(candidate, _)| candidate.same_language(locale))
192 .map(|(_, summary)| summary)
193 })
194 .map_or(self.summary.as_str(), String::as_str)
195 }
196
197 #[must_use]
199 pub fn guidance(mut self, guidance: impl Into<String>) -> Self {
200 self.guidance = Some(guidance.into());
201 self
202 }
203
204 #[must_use]
206 pub const fn target(mut self, policy: TargetPolicy) -> Self {
207 self.target_policy = policy;
208 self
209 }
210
211 #[must_use]
213 pub const fn read_only(mut self) -> Self {
214 self.mutability = ActMutability::ReadOnly;
215 self
216 }
217
218 #[must_use]
220 pub const fn mutating(mut self) -> Self {
221 self.mutability = ActMutability::Mutating;
222 self
223 }
224
225 #[must_use]
227 pub const fn card_only(mut self) -> Self {
228 self.availability = ActAvailability::CardOnly;
229 self
230 }
231
232 #[must_use]
234 pub fn arguments<A: JsonSchema + DeserializeOwned + 'static>(mut self) -> Self {
235 self.arguments_schema = schemars::schema_for!(A);
236 self.arguments = derive_arguments(&self.arguments_schema);
237 self.validator = Some(Arc::new(|value| {
238 serde_json::from_value::<A>(value.clone())
239 .map(|_| ())
240 .map_err(|error| error.to_string())
241 }));
242 self
243 }
244
245 #[must_use]
247 pub fn argument(
248 mut self,
249 name: &str,
250 adjust: impl FnOnce(ArgumentSpec) -> ArgumentSpec,
251 ) -> Self {
252 match self
253 .arguments
254 .iter()
255 .position(|argument| argument.name == name)
256 {
257 Some(index) => {
258 let current = self.arguments.remove(index);
259 self.arguments.insert(index, adjust(current));
260 }
261 None => self
262 .problems
263 .push(format!("`{name}` is not a field of its arguments type")),
264 }
265 self
266 }
267
268 #[must_use]
270 pub fn example(mut self, message: impl Into<String>, arguments: serde_json::Value) -> Self {
271 let arguments = match arguments {
272 serde_json::Value::Object(map) => map,
273 serde_json::Value::Null => serde_json::Map::new(),
274 other => {
275 self.problems.push(format!(
276 "an example's arguments must be an object, not {other}"
277 ));
278 serde_json::Map::new()
279 }
280 };
281 self.examples.push(OperationExample {
282 message: message.into(),
283 arguments,
284 not_given: Vec::new(),
285 });
286 self
287 }
288
289 #[must_use]
291 pub fn example_not_given<'a>(
292 mut self,
293 message: impl Into<String>,
294 names: impl IntoIterator<Item = &'a str>,
295 ) -> Self {
296 self.examples.push(OperationExample {
297 message: message.into(),
298 arguments: serde_json::Map::new(),
299 not_given: names.into_iter().map(str::to_owned).collect(),
300 });
301 self
302 }
303
304 #[must_use]
307 pub fn context_read(mut self, read: impl Into<ReadToolKey>) -> Self {
308 self.context_reads.push(read.into());
309 self
310 }
311
312 #[must_use]
315 pub fn arguments_value(
316 &self,
317 values: impl IntoIterator<Item = (String, serde_json::Value)>,
318 ) -> serde_json::Value {
319 let values: serde_json::Map<String, serde_json::Value> = values.into_iter().collect();
320 let takes_null = self
321 .arguments_schema
322 .as_value()
323 .get("type")
324 .and_then(serde_json::Value::as_str)
325 == Some("null");
326 if takes_null && values.is_empty() {
327 serde_json::Value::Null
328 } else {
329 serde_json::Value::Object(values)
330 }
331 }
332
333 pub fn check_arguments(&self, arguments: &serde_json::Value) -> Result<(), String> {
339 if let Some(validator) = &self.validator {
340 return validator(arguments);
341 }
342 crate::schema::validate_against(&self.arguments_schema, arguments)
343 .map_err(|error| error.to_string())
344 }
345
346 #[must_use]
348 pub fn argument_named(&self, name: &str) -> Option<&ArgumentSpec> {
349 self.arguments.iter().find(|argument| argument.name == name)
350 }
351
352 pub fn validate(&self) -> Result<(), OperationSpecError> {
359 let refuse = |reason: String| OperationSpecError {
360 operation: self.key.clone(),
361 reason,
362 };
363 if let Some(problem) = self.problems.first() {
364 return Err(refuse(problem.clone()));
365 }
366 if self.summary.trim().is_empty() {
367 return Err(refuse("has no summary".to_owned()));
368 }
369 for example in &self.examples {
370 for name in example.not_given.iter().chain(example.arguments.keys()) {
371 if self.argument_named(name).is_none() {
372 return Err(refuse(format!(
373 "example «{}» names `{name}`, which is not an argument",
374 example.message
375 )));
376 }
377 }
378 if example.not_given.is_empty()
379 && let Some(validator) = &self.validator
380 {
381 validator(&serde_json::Value::Object(example.arguments.clone()))
382 .map_err(|error| refuse(format!("example «{}»: {error}", example.message)))?;
383 }
384 }
385 Ok(())
386 }
387}
388
389#[derive(Debug, Clone, Default, PartialEq)]
391pub struct OperationCatalog {
392 operations: indexmap::IndexMap<OperationKey, OperationSpec>,
393}
394
395impl OperationCatalog {
396 pub fn new(
402 operations: impl IntoIterator<Item = OperationSpec>,
403 ) -> Result<Self, crate::error::ReductionError> {
404 let mut catalog = Self::default();
405 for spec in operations {
406 catalog.insert(spec)?;
407 }
408 Ok(catalog)
409 }
410
411 pub fn insert(&mut self, spec: OperationSpec) -> Result<(), crate::error::ReductionError> {
417 if self.operations.contains_key(&spec.key) {
418 return Err(crate::error::ReductionError::DuplicateOperation {
419 operation: spec.key,
420 });
421 }
422 self.operations.insert(spec.key.clone(), spec);
423 Ok(())
424 }
425
426 #[must_use]
428 pub fn get(&self, key: &OperationKey) -> Option<&OperationSpec> {
429 self.operations.get(key)
430 }
431
432 pub fn iter(&self) -> impl Iterator<Item = &OperationSpec> {
434 self.operations.values()
435 }
436
437 #[must_use]
439 pub fn len(&self) -> usize {
440 self.operations.len()
441 }
442
443 #[must_use]
445 pub fn is_empty(&self) -> bool {
446 self.operations.is_empty()
447 }
448}
449
450fn derive_arguments(schema: &Schema) -> Vec<ArgumentSpec> {
452 let value = schema.as_value();
453 let Some(properties) = value
454 .get("properties")
455 .and_then(serde_json::Value::as_object)
456 else {
457 return Vec::new();
458 };
459 let required: Vec<&str> = value
460 .get("required")
461 .and_then(serde_json::Value::as_array)
462 .map(|names| names.iter().filter_map(serde_json::Value::as_str).collect())
463 .unwrap_or_default();
464 let defs = value.get("$defs");
465 properties
466 .iter()
467 .map(|(name, property)| {
468 let mut spec = ArgumentSpec::new(name.clone(), shape_of(property, defs));
469 spec.required = required.contains(&name.as_str());
470 spec.description = property
471 .get("description")
472 .and_then(serde_json::Value::as_str)
473 .map(str::to_owned);
474 spec
475 })
476 .collect()
477}
478
479#[cfg(test)]
480mod tests {
481 use serde_json::json;
482
483 use super::*;
484
485 #[derive(Deserialize, JsonSchema)]
486 #[allow(dead_code)]
487 struct SetSubject {
488 value: String,
490 }
491
492 fn set_subject() -> OperationSpec {
493 OperationSpec::new("trip.set_name")
494 .summary("Name the trip.")
495 .arguments::<SetSubject>()
496 .argument("value", |a| a.label("subject").label_in("it-IT", "oggetto"))
497 }
498
499 #[test]
500 fn arguments_come_from_the_type_with_their_documentation() {
501 let spec = set_subject();
502 let value = spec.argument_named("value").unwrap();
503 assert!(value.required);
504 assert_eq!(
505 value.description.as_deref(),
506 Some("What the trip is called.")
507 );
508 assert_eq!(value.shape, ValueShape::Text { written: false });
509 assert!(spec.validate().is_ok());
510 }
511
512 #[test]
513 fn an_example_that_does_not_fit_the_arguments_type_is_refused() {
514 let wrong = set_subject().example("the subject is March", json!({"value": 3}));
515 assert!(wrong.validate().is_err());
516 let unknown = set_subject().example_not_given("set the subject", ["title"]);
517 assert!(unknown.validate().is_err());
518 let fine = set_subject()
519 .example("the subject is March", json!({"value": "March"}))
520 .example_not_given("the subject needs changing", ["value"]);
521 assert!(fine.validate().is_ok());
522 }
523
524 #[test]
525 fn adjusting_an_argument_the_type_does_not_have_is_refused() {
526 let spec = set_subject().argument("title", |a| a.label("title"));
527 assert!(spec.validate().unwrap_err().reason.contains("`title`"));
528 }
529
530 #[test]
531 fn a_summary_is_read_in_the_turns_language_when_it_has_one() {
532 let spec = OperationSpec::new("trip.set_name")
533 .summary("Name the trip.")
534 .summary_in("it-IT", "Dà un nome al viaggio.");
535 assert_eq!(
536 spec.summary_for(&Locale::from("it-IT")),
537 "Dà un nome al viaggio."
538 );
539 assert_eq!(
540 spec.summary_for(&Locale::from("it-CH")),
541 "Dà un nome al viaggio."
542 );
543 assert_eq!(spec.summary_for(&Locale::from("de-DE")), "Name the trip.");
544 }
545}