1use serde::Serialize;
4
5use crate::diagnostic::SchemaDiagnostic;
6use crate::frontmatter::Frontmatter;
7use crate::item::DefinitionKind;
8use crate::ordered::OrderedValue;
9
10#[derive(Debug, Clone, Serialize)]
15#[serde(untagged)]
16pub enum Reading {
17 Manifest(DefinitionRead<crate::package::PackageMetadata>),
18 Agent(DefinitionRead<crate::agent::AgentProfile>),
19 Skill(DefinitionRead<crate::skill::SkillProfile>),
20}
21
22impl Reading {
23 pub fn diagnostics(&self) -> &[SchemaDiagnostic] {
24 match self {
25 Self::Manifest(reading) => &reading.diagnostics,
26 Self::Agent(reading) => &reading.diagnostics,
27 Self::Skill(reading) => &reading.diagnostics,
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34#[error(
35 "`{0}` is not a schema file: expected `mars.toml`, `agents/<slug>.md` or `skills/<slug>/SKILL.md`"
36)]
37pub struct UnrecognizedPath(pub String);
38
39pub fn installed_agent(text: &str) -> DefinitionRead<crate::agent::AgentProfile> {
50 installed(text, DefinitionKind::Agent, crate::agent::read)
51}
52
53pub fn installed_skill(text: &str) -> DefinitionRead<crate::skill::SkillProfile> {
55 installed(text, DefinitionKind::Skill, crate::skill::read)
56}
57
58fn installed<T>(
59 text: &str,
60 kind: DefinitionKind,
61 strict: impl Fn(&str) -> DefinitionRead<T>,
62) -> DefinitionRead<T> {
63 let reading = strict(text);
64 let pre_v1 = reading.diagnostics.iter().any(|diagnostic| {
67 matches!(
68 diagnostic.code.as_str(),
69 "field.removed" | "tools.list-form"
70 )
71 });
72 if !pre_v1 {
73 return reading;
74 }
75 match crate::upgrade::upgrade_definition(kind, text) {
76 Ok(upgraded) => strict(&upgraded.text),
77 Err(_) => reading,
79 }
80}
81
82pub fn read(path: &str, text: &str) -> Result<Reading, UnrecognizedPath> {
84 let parts: Vec<&str> = path.split('/').collect();
85 match parts.as_slice() {
86 ["mars.toml"] => Ok(Reading::Manifest(crate::manifest::read(text))),
87 ["agents", file]
88 if file
89 .strip_suffix(".md")
90 .is_some_and(|slug| !slug.is_empty()) =>
91 {
92 Ok(read_definition(DefinitionKind::Agent, text))
93 }
94 ["skills", slug, "SKILL.md"] if !slug.is_empty() => {
95 Ok(read_definition(DefinitionKind::Skill, text))
96 }
97 _ => Err(UnrecognizedPath(path.to_owned())),
98 }
99}
100
101pub(crate) fn read_definition(kind: DefinitionKind, text: &str) -> Reading {
106 match kind {
107 DefinitionKind::Agent => Reading::Agent(crate::agent::read(text)),
108 DefinitionKind::Skill => Reading::Skill(crate::skill::read(text)),
109 }
110}
111
112pub(crate) fn field_diagnostics(kind: DefinitionKind, fm: &Frontmatter) -> Vec<SchemaDiagnostic> {
114 let mut diagnostics = Vec::new();
115 match kind {
116 DefinitionKind::Agent => {
117 crate::agent::read_fields(fm, &mut diagnostics);
118 }
119 DefinitionKind::Skill => {
120 crate::skill::read_fields(fm, &mut diagnostics);
121 }
122 }
123 diagnostics
124}
125
126pub(crate) fn read_with<T>(
130 text: &str,
131 fields: impl FnOnce(&Frontmatter, &mut Vec<SchemaDiagnostic>) -> T,
132) -> DefinitionRead<T> {
133 match Frontmatter::parse_preserving_structure(text) {
134 Ok((frontmatter, structural_errors)) => {
135 let mut diagnostics = structural_errors
136 .iter()
137 .map(crate::diagnostic::frontmatter_error)
138 .collect::<Vec<_>>();
139 let fields = fields(&frontmatter, &mut diagnostics);
140 DefinitionRead {
141 frontmatter: frontmatter_json(frontmatter.mapping(), &mut diagnostics),
142 fields: Some(fields),
143 body: Some(frontmatter.body().to_string()),
144 diagnostics,
145 }
146 }
147 Err(error) => DefinitionRead::unread(vec![crate::diagnostic::frontmatter_error(&error)]),
148 }
149}
150
151#[derive(Debug, Clone, Serialize)]
152pub struct DefinitionRead<T> {
153 pub frontmatter: Vec<(String, OrderedValue)>,
155 pub fields: Option<T>,
156 pub body: Option<String>,
157 pub diagnostics: Vec<SchemaDiagnostic>,
158}
159
160#[derive(Debug, Clone)]
162pub struct Valid<T> {
163 pub fields: T,
164 pub body: String,
165 pub warnings: Vec<SchemaDiagnostic>,
166}
167
168impl<T> DefinitionRead<T> {
169 pub(crate) fn unread(diagnostics: Vec<SchemaDiagnostic>) -> Self {
171 Self {
172 frontmatter: Vec::new(),
173 fields: None,
174 body: None,
175 diagnostics,
176 }
177 }
178
179 pub fn into_fields(self) -> Result<T, String> {
181 self.fields.ok_or_else(|| {
182 self.diagnostics
183 .first()
184 .map_or("unreadable".to_owned(), |diagnostic| {
185 diagnostic.message.clone()
186 })
187 })
188 }
189
190 pub fn into_valid(self) -> Result<Valid<T>, SchemaDiagnostic> {
193 if let Some(error) = self
194 .diagnostics
195 .iter()
196 .find(|diagnostic| diagnostic.is_error())
197 {
198 return Err(error.clone());
199 }
200 match (self.fields, self.body) {
201 (Some(fields), Some(body)) => Ok(Valid {
202 fields,
203 body,
204 warnings: self.diagnostics,
205 }),
206 _ => unreachable!("a reading with no error has its fields and body"),
207 }
208 }
209}
210
211pub(crate) fn frontmatter_json(
212 mapping: &serde_yaml::Mapping,
213 diagnostics: &mut Vec<SchemaDiagnostic>,
214) -> Vec<(String, OrderedValue)> {
215 mapping
216 .iter()
217 .filter_map(|(key, value)| {
218 let Some(key) = key.as_str() else {
219 diagnostics.push(SchemaDiagnostic::new(
220 "yaml.non-string-key",
221 crate::diagnostic::Severity::Error,
222 Vec::new(),
223 "frontmatter keys must be strings",
224 ));
225 return None;
226 };
227 match OrderedValue::from_yaml(value) {
228 Ok(value) => Some((key.to_owned(), value)),
229 Err(message) => {
230 diagnostics.push(SchemaDiagnostic::new(
231 "yaml.non-json-value",
232 crate::diagnostic::Severity::Error,
233 vec![crate::diagnostic::PathSegment::Key(key.to_owned())],
234 message,
235 ));
236 None
237 }
238 }
239 })
240 .collect()
241}