use serde::{Deserialize, Serialize};
use crate::ids::{ReadToolKey, WorkflowKey};
use crate::locale::Locale;
use crate::operation::value::DateDirection;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ArgumentSource {
#[default]
User,
Inferred,
Server {
read: ReadToolKey,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ValueShape {
Text {
written: bool,
},
Enum {
values: Vec<String>,
},
Integer,
Number,
Bool,
Date {
direction: DateDirection,
},
Money,
Record {
workflow: WorkflowKey,
},
Structured,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArgumentLabel {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locale: Option<Locale>,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ArgumentSpec {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<ArgumentLabel>,
pub required: bool,
pub source: ArgumentSource,
pub shape: ValueShape,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub names_the_record: bool,
}
impl ArgumentSpec {
#[must_use]
pub fn new(name: impl Into<String>, shape: ValueShape) -> Self {
Self {
name: name.into(),
description: None,
labels: Vec::new(),
required: false,
source: ArgumentSource::User,
shape,
names_the_record: false,
}
}
#[must_use]
pub fn label(mut self, text: impl Into<String>) -> Self {
self.labels.push(ArgumentLabel {
locale: None,
text: text.into(),
});
self
}
#[must_use]
pub fn label_in(mut self, locale: impl Into<Locale>, text: impl Into<String>) -> Self {
self.labels.push(ArgumentLabel {
locale: Some(locale.into()),
text: text.into(),
});
self
}
#[must_use]
pub fn describe(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub const fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub const fn names_the_record(mut self) -> Self {
self.names_the_record = true;
self
}
#[must_use]
pub const fn optional(mut self) -> Self {
self.required = false;
self
}
#[must_use]
pub fn inferred(mut self) -> Self {
self.source = ArgumentSource::Inferred;
self
}
#[must_use]
pub fn from_read(mut self, read: impl Into<ReadToolKey>) -> Self {
self.source = ArgumentSource::Server { read: read.into() };
self
}
#[must_use]
pub fn written(mut self) -> Self {
if let ValueShape::Text { written } = &mut self.shape {
*written = true;
}
self
}
#[must_use]
pub fn date_direction(mut self, direction: DateDirection) -> Self {
if let ValueShape::Date { direction: current } = &mut self.shape {
*current = direction;
}
self
}
#[must_use]
pub fn money(mut self) -> Self {
self.shape = ValueShape::Money;
self
}
#[must_use]
pub fn record(mut self, workflow: impl Into<WorkflowKey>) -> Self {
self.shape = ValueShape::Record {
workflow: workflow.into(),
};
self
}
pub fn labels_for<'a>(&'a self, locale: &'a Locale) -> impl Iterator<Item = &'a str> + 'a {
let own = self
.labels
.iter()
.filter(move |label| label.locale.as_ref() == Some(locale));
let universal = self.labels.iter().filter(|label| label.locale.is_none());
own.chain(universal).map(|label| label.text.as_str())
}
}
#[must_use]
pub fn shape_of(property: &serde_json::Value, defs: Option<&serde_json::Value>) -> ValueShape {
if let Some(value) = without_null(property) {
return shape_of(&value, defs);
}
let resolved = resolve_ref(property, defs).unwrap_or(property);
if let Some(values) = enum_values(resolved) {
return ValueShape::Enum { values };
}
let kind = resolved.get("type").and_then(serde_json::Value::as_str);
match kind {
Some("string")
if resolved.get("format").and_then(serde_json::Value::as_str) == Some("date") =>
{
ValueShape::Date {
direction: DateDirection::Any,
}
}
Some("string") => ValueShape::Text { written: false },
Some("integer") => ValueShape::Integer,
Some("number") => ValueShape::Number,
Some("boolean") => ValueShape::Bool,
_ => ValueShape::Structured,
}
}
fn without_null(property: &serde_json::Value) -> Option<serde_json::Value> {
let is_null = |kind: &serde_json::Value| kind.as_str() == Some("null");
if let Some(kinds) = property.get("type").and_then(serde_json::Value::as_array) {
let kept: Vec<&serde_json::Value> = kinds.iter().filter(|kind| !is_null(kind)).collect();
let [only] = kept.as_slice() else {
return None;
};
let mut value = property.clone();
value["type"] = (*only).clone();
return Some(value);
}
let branches = property
.get("anyOf")
.or_else(|| property.get("oneOf"))?
.as_array()?;
let kept: Vec<serde_json::Value> = branches
.iter()
.filter(|branch| !branch.get("type").is_some_and(is_null))
.cloned()
.collect();
match kept.as_slice() {
_ if kept.len() == branches.len() => None,
[only] => Some(only.clone()),
_ => Some(serde_json::json!({ "anyOf": kept })),
}
}
fn resolve_ref<'a>(
property: &'a serde_json::Value,
defs: Option<&'a serde_json::Value>,
) -> Option<&'a serde_json::Value> {
let reference = property.get("$ref")?.as_str()?;
let name = reference.rsplit('/').next()?;
defs?.get(name)
}
fn enum_values(schema: &serde_json::Value) -> Option<Vec<String>> {
if let Some(values) = schema.get("enum").and_then(serde_json::Value::as_array) {
return values
.iter()
.map(|value| value.as_str().map(str::to_owned))
.collect();
}
let branches = schema
.get("oneOf")
.or_else(|| schema.get("anyOf"))?
.as_array()?;
branches
.iter()
.map(|branch| branch.get("const")?.as_str().map(str::to_owned))
.collect()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn an_optional_argument_has_the_shape_of_its_value() {
let defs = json!({"Kind": {"enum": ["a", "b"]}});
let text = ValueShape::Text { written: false };
assert_eq!(shape_of(&json!({"type": ["string", "null"]}), None), text);
assert_eq!(
shape_of(
&json!({"anyOf": [{"type": "string"}, {"type": "null"}]}),
None
),
text
);
assert_eq!(
shape_of(
&json!({"anyOf": [{"$ref": "#/$defs/Kind"}, {"type": "null"}]}),
Some(&defs)
),
ValueShape::Enum {
values: vec!["a".into(), "b".into()]
}
);
assert_eq!(
shape_of(&json!({"type": ["integer", "null"]}), None),
ValueShape::Integer
);
}
#[test]
fn shapes_follow_the_argument_types_schema() {
let defs = json!({"Kind": {"oneOf": [{"type": "string", "const": "a"}, {"type": "string", "const": "b"}]}});
assert_eq!(
shape_of(&json!({"type": "string"}), None),
ValueShape::Text { written: false }
);
assert_eq!(
shape_of(&json!({"type": "string", "format": "date"}), None),
ValueShape::Date {
direction: DateDirection::Any
}
);
assert_eq!(
shape_of(&json!({"$ref": "#/$defs/Kind"}), Some(&defs)),
ValueShape::Enum {
values: vec!["a".into(), "b".into()]
}
);
assert_eq!(
shape_of(&json!({"type": "integer"}), None),
ValueShape::Integer
);
assert_eq!(
shape_of(&json!({"type": "object"}), None),
ValueShape::Structured
);
}
#[test]
fn labels_in_the_turns_language_come_first() {
let spec = ArgumentSpec::new("value", ValueShape::Text { written: false })
.label("subject")
.label_in("it-IT", "oggetto");
let (it, en) = (Locale::from("it-IT"), Locale::from("en-GB"));
let italian: Vec<&str> = spec.labels_for(&it).collect();
assert_eq!(italian, vec!["oggetto", "subject"]);
let english: Vec<&str> = spec.labels_for(&en).collect();
assert_eq!(english, vec!["subject"]);
}
}