use serde::Serialize;
use crate::diagnostic::SchemaDiagnostic;
use crate::frontmatter::Frontmatter;
use crate::item::DefinitionKind;
use crate::ordered::OrderedValue;
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum Reading {
Manifest(crate::manifest::ManifestRead),
Agent(DefinitionRead<crate::agent::AgentProfile>),
Skill(DefinitionRead<crate::skill::SkillProfile>),
}
impl Reading {
pub fn diagnostics(&self) -> &[SchemaDiagnostic] {
match self {
Self::Manifest(reading) => &reading.package.diagnostics,
Self::Agent(reading) => &reading.diagnostics,
Self::Skill(reading) => &reading.diagnostics,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"`{0}` is not a schema file: expected `mars.toml`, `agents/<slug>.md` or `skills/<slug>/SKILL.md`"
)]
pub struct UnrecognizedPath(pub String);
impl UnrecognizedPath {
pub fn code(&self) -> &'static str {
"read.unrecognized-path"
}
}
pub fn installed_agent(text: &str) -> DefinitionRead<crate::agent::AgentProfile> {
installed(text, DefinitionKind::Agent, crate::agent::read)
}
pub fn installed_skill(text: &str) -> DefinitionRead<crate::skill::SkillProfile> {
installed(text, DefinitionKind::Skill, crate::skill::read)
}
fn installed<T>(
text: &str,
kind: DefinitionKind,
strict: impl Fn(&str) -> DefinitionRead<T>,
) -> DefinitionRead<T> {
let reading = strict(text);
let pre_v1 = reading.diagnostics.iter().any(|diagnostic| {
matches!(
diagnostic.code.as_str(),
"field.removed" | "tools.list-form"
)
});
if !pre_v1 {
return reading;
}
match crate::upgrade::upgrade_definition(kind, text) {
Ok(upgraded) => strict(&upgraded.text),
Err(_) => reading,
}
}
pub fn read(path: &str, text: &str) -> Result<Reading, UnrecognizedPath> {
let parts: Vec<&str> = path.split('/').collect();
match parts.as_slice() {
["mars.toml"] => Ok(Reading::Manifest(crate::manifest::read(text))),
["agents", file]
if file
.strip_suffix(".md")
.is_some_and(|slug| !slug.is_empty()) =>
{
Ok(read_definition(DefinitionKind::Agent, text))
}
["skills", slug, "SKILL.md"] if !slug.is_empty() => {
Ok(read_definition(DefinitionKind::Skill, text))
}
_ => Err(UnrecognizedPath(path.to_owned())),
}
}
pub(crate) fn read_definition(kind: DefinitionKind, text: &str) -> Reading {
match kind {
DefinitionKind::Agent => Reading::Agent(crate::agent::read(text)),
DefinitionKind::Skill => Reading::Skill(crate::skill::read(text)),
}
}
pub(crate) fn field_diagnostics(kind: DefinitionKind, fm: &Frontmatter) -> Vec<SchemaDiagnostic> {
let mut diagnostics = Vec::new();
match kind {
DefinitionKind::Agent => {
crate::agent::read_fields(fm, &mut diagnostics);
}
DefinitionKind::Skill => {
crate::skill::read_fields(fm, &mut diagnostics);
}
}
diagnostics
}
pub(crate) fn read_with<T>(
text: &str,
fields: impl FnOnce(&Frontmatter, &mut Vec<SchemaDiagnostic>) -> T,
) -> DefinitionRead<T> {
match Frontmatter::parse_preserving_structure(text) {
Ok((frontmatter, structural_errors)) => {
let mut diagnostics = structural_errors
.iter()
.map(crate::diagnostic::frontmatter_error)
.collect::<Vec<_>>();
let fields = fields(&frontmatter, &mut diagnostics);
DefinitionRead {
frontmatter: frontmatter_json(frontmatter.mapping(), &mut diagnostics),
fields: Some(fields),
body: Some(frontmatter.body().to_string()),
diagnostics,
}
}
Err(error) => DefinitionRead::unread(vec![crate::diagnostic::frontmatter_error(&error)]),
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DefinitionRead<T> {
pub frontmatter: Vec<(String, OrderedValue)>,
pub fields: Option<T>,
pub body: Option<String>,
pub diagnostics: Vec<SchemaDiagnostic>,
}
#[derive(Debug, Clone)]
pub struct Valid<T> {
pub fields: T,
pub body: String,
pub warnings: Vec<SchemaDiagnostic>,
}
impl<T> DefinitionRead<T> {
pub(crate) fn unread(diagnostics: Vec<SchemaDiagnostic>) -> Self {
Self {
frontmatter: Vec::new(),
fields: None,
body: None,
diagnostics,
}
}
pub fn into_fields(self) -> Result<T, String> {
self.fields.ok_or_else(|| {
self.diagnostics
.first()
.map_or("unreadable".to_owned(), |diagnostic| {
diagnostic.message.clone()
})
})
}
pub fn into_valid(self) -> Result<Valid<T>, SchemaDiagnostic> {
if let Some(error) = self
.diagnostics
.iter()
.find(|diagnostic| diagnostic.is_error())
{
return Err(error.clone());
}
match (self.fields, self.body) {
(Some(fields), Some(body)) => Ok(Valid {
fields,
body,
warnings: self.diagnostics,
}),
_ => unreachable!("a reading with no error has its fields and body"),
}
}
}
pub(crate) fn frontmatter_json(
mapping: &serde_yaml::Mapping,
diagnostics: &mut Vec<SchemaDiagnostic>,
) -> Vec<(String, OrderedValue)> {
mapping
.iter()
.filter_map(|(key, value)| {
let Some(key) = key.as_str() else {
diagnostics.push(SchemaDiagnostic::new(
"yaml.non-string-key",
crate::diagnostic::Severity::Error,
Vec::new(),
"frontmatter keys must be strings",
));
return None;
};
match OrderedValue::from_yaml(value) {
Ok(value) => Some((key.to_owned(), value)),
Err(message) => {
diagnostics.push(SchemaDiagnostic::new(
"yaml.non-json-value",
crate::diagnostic::Severity::Error,
vec![crate::diagnostic::PathSegment::Key(key.to_owned())],
message,
));
None
}
}
})
.collect()
}