use std::{fmt, marker::PhantomData};
use serde::{
Deserialize, Deserializer,
de::{self, MapAccess, Visitor},
};
use serde_json::Value;
use crate::hook::{Event, ToolKind};
#[derive(Debug, Deserialize)]
pub(crate) struct VersionOnly {
pub version: u64,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(untagged))]
#[derive(Debug, Clone)]
pub(crate) enum Text {
Inline(String),
File(FileRef),
From(FromRef),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct FromRef {
pub from: String,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(untagged))]
#[derive(Debug, Clone)]
pub(crate) enum Many<T> {
List(Vec<T>),
From(FromRef),
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Many<T> {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V<T>(PhantomData<T>);
impl<'de, T: Deserialize<'de>> Visitor<'de> for V<T> {
type Value = Many<T>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a list or { from = \"<path>\" }")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Many<T>, A::Error> {
Vec::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(Many::List)
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Many<T>, A::Error> {
FromRef::deserialize(de::value::MapAccessDeserializer::new(map)).map(Many::From)
}
}
d.deserialize_any(V(PhantomData))
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct FileRef {
pub file: String,
}
impl<'de> Deserialize<'de> for Text {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Text;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a string, { file = \"<path>\" } or { from = \"<path>\" }")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Text, E> {
Ok(Text::Inline(s.to_string()))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Text, A::Error> {
let key: String = map
.next_key()?
.ok_or_else(|| de::Error::custom("expected `file` or `from`"))?;
let path: String = map.next_value()?;
if let Some(other) = map.next_key::<String>()? {
return Err(de::Error::custom(format!("unknown field `{other}`")));
}
match key.as_str() {
"file" => Ok(Text::File(FileRef { file: path })),
"from" => Ok(Text::From(FromRef { from: path })),
k => Err(de::Error::unknown_field(k, &["file", "from"])),
}
}
}
d.deserialize_any(V)
}
}
impl<'de> Deserialize<'de> for Harnesses {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Harnesses;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\"all\" or a list of harness ids")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Harnesses, E> {
Ok(Harnesses::All(s.to_string()))
}
fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Harnesses, A::Error> {
Vec::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(Harnesses::List)
}
}
d.deserialize_any(V)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Ordered<V>(pub Vec<(String, V)>);
impl<'de, V: Deserialize<'de>> Deserialize<'de> for Ordered<V> {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct Pairs<V>(PhantomData<V>);
impl<'de, V: Deserialize<'de>> Visitor<'de> for Pairs<V> {
type Value = Ordered<V>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a table")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut out = Vec::new();
while let Some(kv) = map.next_entry::<String, V>()? {
out.push(kv);
}
Ok(Ordered(out))
}
}
d.deserialize_map(Pairs(PhantomData))
}
}
#[cfg(feature = "schemars")]
impl<V: schemars::JsonSchema> schemars::JsonSchema for Ordered<V> {
fn schema_name() -> std::borrow::Cow<'static, str> {
format!("Ordered_{}", V::schema_name()).into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
<std::collections::BTreeMap<String, V>>::json_schema(generator)
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(untagged))]
#[derive(Debug, Clone)]
pub(crate) enum Harnesses {
All(String),
List(Vec<String>),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase", deny_unknown_fields)]
pub(crate) enum Match {
Prefix(String),
Contains(String),
Any(Vec<Match>),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct SkillFile {
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub body: Option<Text>,
#[serde(default)]
pub files: Option<Ordered<Text>>,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct HookFile {
pub event: Event,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub run: Option<String>,
#[serde(default)]
pub tools: Option<ToolKind>,
#[serde(default)]
pub timeout: Option<u64>,
#[serde(default)]
pub commands: Option<Ordered<String>>,
#[serde(default)]
pub owned: Option<Match>,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct McpFile {
pub name: String,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub args: Option<Vec<String>>,
#[serde(default)]
pub env: Option<Ordered<String>>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub headers: Option<Ordered<String>>,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct McpToolFile {
pub server: String,
pub tool: String,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct PromptFile {
pub name: String,
pub description: String,
pub prompt: Text,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
pub(crate) enum PartFile {
File {
harness: String,
name: String,
dir: String,
files: Ordered<Text>,
},
Region {
harness: String,
name: String,
file: String,
block: Text,
},
Merge {
harness: String,
name: String,
file: String,
ops: Vec<OpFile>,
},
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
pub(crate) enum OpFile {
ArrayEntry {
path: Vec<String>,
value: Value,
},
ObjectMember {
path: Vec<String>,
key: String,
value: Value,
},
OwnedEntries {
path: Vec<String>,
field: String,
owned: Match,
entries: Vec<Value>,
},
GroupEntries {
path: Vec<String>,
entries: String,
field: String,
owned: Match,
groups: Vec<Value>,
},
}
macro_rules! with_items {
($($(#[doc = $doc:literal])* $field:ident: $ty:ty,)*) => {
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct Items<'a> {
$(pub $field: Option<&'a $ty>,)*
}
impl<'a> Items<'a> {
pub fn with(self, scope: Items<'a>) -> Items<'a> {
Items { $($field: scope.$field.or(self.$field),)* }
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(title = "AHA manifest"))]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ManifestFile {
pub version: u64,
pub name: String,
#[serde(default)]
pub harnesses: Option<Harnesses>,
#[serde(default)]
pub adapter: Option<String>,
#[serde(default)]
pub record: Option<String>,
#[serde(default)]
pub declined: Option<String>,
$($(#[doc = $doc])* #[serde(default)] pub $field: Option<$ty>,)*
#[serde(default)]
pub scopes: Option<ScopesFile>,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ScopeFile {
#[serde(default)]
pub record: Option<String>,
#[serde(default)]
pub declined: Option<String>,
$($(#[doc = $doc])* #[serde(default)] pub $field: Option<$ty>,)*
}
impl ManifestFile {
pub fn items(&self) -> Items<'_> {
Items { $($field: self.$field.as_ref(),)* }
}
}
impl ScopeFile {
pub fn items(&self) -> Items<'_> {
Items { $($field: self.$field.as_ref(),)* }
}
}
};
}
with_items! {
instructions: Text,
hook_match: Match,
skills: Many<SkillFile>,
hooks: Many<HookFile>,
mcp_servers: Many<McpFile>,
allow_commands: Vec<String>,
allow_mcp_tools: Vec<McpToolFile>,
agents: Many<PromptFile>,
commands: Vec<PromptFile>,
parts: Vec<PartFile>,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ScopesFile {
#[serde(default)]
pub project: Option<ScopeFile>,
#[serde(default)]
pub user: Option<ScopeFile>,
#[serde(default)]
pub local: Option<ScopeFile>,
}