1use std::fmt;
2
3use runx_contracts::{JsonObject, JsonValue, json_string_field};
4use runx_parser::{
5 ParseError, SkillInstallError, SkillInstallOrigin, ValidateSkillMode, ValidateSkillOptions,
6 ValidationError, extract_skill_quality_profile, parse_graph_yaml, parse_runner_manifest_yaml,
7 parse_skill_markdown, parse_tool_manifest_json, runner::resolve_post_run_reflect_policy,
8 validate_graph, validate_runner_manifest, validate_skill_artifact_contract,
9 validate_skill_install, validate_skill_source, validate_skill_with_options,
10 validate_tool_manifest,
11};
12use serde::{Deserialize, Serialize};
13
14#[derive(Clone, Debug, PartialEq, Serialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum ParserEvalOutput {
17 Output { value: JsonValue },
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum ParserEvalError {
22 InvalidDocument(String),
23 InvalidInput(String),
24 Parse(String),
25 Validation(String),
26 SerializeOutput(String),
27}
28
29impl ParserEvalError {
30 #[must_use]
31 pub fn code(&self) -> &'static str {
32 match self {
33 Self::InvalidDocument(_) => "invalid_document",
34 Self::InvalidInput(_) => "invalid_input",
35 Self::Parse(_) => "parse_error",
36 Self::Validation(_) => "validation_error",
37 Self::SerializeOutput(_) => "serialize_output",
38 }
39 }
40}
41
42impl fmt::Display for ParserEvalError {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::InvalidDocument(message)
46 | Self::InvalidInput(message)
47 | Self::Parse(message)
48 | Self::Validation(message)
49 | Self::SerializeOutput(message) => formatter.write_str(message),
50 }
51 }
52}
53
54impl std::error::Error for ParserEvalError {}
55
56impl From<ParseError> for ParserEvalError {
57 fn from(error: ParseError) -> Self {
58 Self::Parse(error.to_string())
59 }
60}
61
62impl From<ValidationError> for ParserEvalError {
63 fn from(error: ValidationError) -> Self {
64 Self::Validation(error.to_string())
65 }
66}
67
68impl From<SkillInstallError> for ParserEvalError {
69 fn from(error: SkillInstallError) -> Self {
70 Self::Validation(error.to_string())
71 }
72}
73
74pub fn evaluate_parser_document_str(source: &str) -> Result<ParserEvalOutput, ParserEvalError> {
75 let document = serde_json::from_str::<JsonValue>(source)
76 .map_err(|error| ParserEvalError::InvalidDocument(error.to_string()))?;
77 if let Some(kind) = parser_document_kind(&document)
78 && !is_supported_parser_kind(kind)
79 {
80 return Err(ParserEvalError::InvalidInput(format!(
81 "unsupported parser input kind '{kind}'"
82 )));
83 }
84 let input = serde_json::from_str::<ParserDocument>(source)
85 .map_err(|error| ParserEvalError::InvalidInput(error.to_string()))?;
86 Ok(ParserEvalOutput::Output {
87 value: evaluate_parser_input(input)?,
88 })
89}
90
91fn parser_document_kind(document: &JsonValue) -> Option<&str> {
92 let JsonValue::Object(fields) = document else {
93 return None;
94 };
95 match fields.get("input") {
96 Some(JsonValue::Object(input)) => json_string_field(input, "kind"),
97 _ => json_string_field(fields, "kind"),
98 }
99}
100
101fn is_supported_parser_kind(kind: &str) -> bool {
102 matches!(
103 kind,
104 "parser.validateSkillMarkdown"
105 | "parser.validateRunnerManifestYaml"
106 | "parser.validateGraphYaml"
107 | "parser.validateToolManifestJson"
108 | "parser.validateSkillSource"
109 | "parser.validateSkillArtifactContract"
110 | "parser.extractSkillQualityProfile"
111 | "parser.resolvePostRunReflectPolicy"
112 | "parser.validateSkillInstall"
113 )
114}
115
116#[derive(Debug, Deserialize)]
117#[serde(untagged)]
118enum ParserDocument {
119 Envelope { input: ParserInput },
120 Input(ParserInput),
121}
122
123impl From<ParserDocument> for ParserInput {
124 fn from(document: ParserDocument) -> Self {
125 match document {
126 ParserDocument::Envelope { input } | ParserDocument::Input(input) => input,
127 }
128 }
129}
130
131#[derive(Debug, Deserialize)]
132#[serde(tag = "kind", rename_all_fields = "camelCase")]
133enum ParserInput {
134 #[serde(rename = "parser.validateSkillMarkdown")]
135 ValidateSkillMarkdown {
136 markdown: String,
137 #[serde(default)]
138 mode: ParserSkillMode,
139 },
140 #[serde(rename = "parser.validateRunnerManifestYaml")]
141 ValidateRunnerManifestYaml { yaml: String },
142 #[serde(rename = "parser.validateGraphYaml")]
143 ValidateGraphYaml { yaml: String },
144 #[serde(rename = "parser.validateToolManifestJson")]
145 ValidateToolManifestJson { json: String },
146 #[serde(rename = "parser.validateSkillSource")]
147 ValidateSkillSource {
148 source: JsonObject,
149 #[serde(default)]
150 runx: Option<JsonObject>,
151 },
152 #[serde(rename = "parser.validateSkillArtifactContract")]
153 ValidateSkillArtifactContract {
154 #[serde(default)]
155 artifacts: Option<JsonValue>,
156 #[serde(default = "default_artifact_field")]
157 field: String,
158 },
159 #[serde(rename = "parser.extractSkillQualityProfile")]
160 ExtractSkillQualityProfile { body: String },
161 #[serde(rename = "parser.resolvePostRunReflectPolicy")]
162 ResolvePostRunReflectPolicy {
163 #[serde(default)]
164 runx: Option<JsonObject>,
165 #[serde(default = "default_runx_field")]
166 field: String,
167 },
168 #[serde(rename = "parser.validateSkillInstall")]
169 ValidateSkillInstall {
170 markdown: String,
171 origin: SkillInstallOrigin,
172 },
173}
174
175#[derive(Clone, Copy, Debug, Default, Deserialize)]
176#[serde(rename_all = "snake_case")]
177enum ParserSkillMode {
178 #[default]
179 Strict,
180 Lenient,
181}
182
183impl From<ParserSkillMode> for ValidateSkillOptions {
184 fn from(mode: ParserSkillMode) -> Self {
185 match mode {
186 ParserSkillMode::Strict => Self {
187 mode: ValidateSkillMode::Strict,
188 },
189 ParserSkillMode::Lenient => Self {
190 mode: ValidateSkillMode::Lenient,
191 },
192 }
193 }
194}
195
196fn evaluate_parser_input(input: ParserDocument) -> Result<JsonValue, ParserEvalError> {
197 match ParserInput::from(input) {
198 ParserInput::ValidateSkillMarkdown { markdown, mode } => {
199 let raw = parse_skill_markdown(&markdown)?;
200 to_json_value(validate_skill_with_options(raw, mode.into())?)
201 }
202 ParserInput::ValidateRunnerManifestYaml { yaml } => {
203 let raw = parse_runner_manifest_yaml(&yaml)?;
204 to_json_value(validate_runner_manifest(raw)?)
205 }
206 ParserInput::ValidateGraphYaml { yaml } => {
207 let raw = parse_graph_yaml(&yaml)?;
208 to_json_value(validate_graph(raw)?)
209 }
210 ParserInput::ValidateToolManifestJson { json } => {
211 let raw = parse_tool_manifest_json(&json)?;
212 to_json_value(validate_tool_manifest(raw)?)
213 }
214 ParserInput::ValidateSkillSource { source, runx } => {
215 to_json_value(validate_skill_source(&source, runx.as_ref())?)
216 }
217 ParserInput::ValidateSkillArtifactContract { artifacts, field } => to_json_value(
218 validate_skill_artifact_contract(artifacts.as_ref(), &field)?,
219 ),
220 ParserInput::ExtractSkillQualityProfile { body } => {
221 to_json_value(extract_skill_quality_profile(&body))
222 }
223 ParserInput::ResolvePostRunReflectPolicy { runx, field } => {
224 to_json_value(resolve_post_run_reflect_policy(runx.as_ref(), &field)?)
225 }
226 ParserInput::ValidateSkillInstall { markdown, origin } => {
227 to_json_value(validate_skill_install(&markdown, origin)?)
228 }
229 }
230}
231
232fn to_json_value<T: Serialize>(value: T) -> Result<JsonValue, ParserEvalError> {
233 let serialized = serde_json::to_value(value)
234 .map_err(|error| ParserEvalError::SerializeOutput(error.to_string()))?;
235 serde_json::from_value(serialized)
236 .map_err(|error| ParserEvalError::SerializeOutput(error.to_string()))
237}
238
239fn default_artifact_field() -> String {
240 "runx.artifacts".to_owned()
241}
242
243fn default_runx_field() -> String {
244 "runx".to_owned()
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn evaluates_skill_markdown_validation() -> Result<(), String> {
253 let output = evaluate_parser_document_str(
254 r#"{
255 "kind": "parser.validateSkillMarkdown",
256 "markdown": "---\nname: parser-demo\n---\n# Parser Demo\n",
257 "mode": "strict"
258 }"#,
259 )
260 .map_err(|error| error.to_string())?;
261 let ParserEvalOutput::Output { value } = output;
262 let JsonValue::Object(skill) = value else {
263 return Err("expected validated skill object".into());
264 };
265 assert_eq!(
266 skill.get("name"),
267 Some(&JsonValue::String("parser-demo".to_owned()))
268 );
269 Ok(())
270 }
271
272 #[test]
273 fn rejects_unsupported_parser_kind_before_deserializing() -> Result<(), String> {
274 let error = match evaluate_parser_document_str(r#"{"kind":"parser.unknown"}"#) {
275 Ok(_) => return Err("unsupported parser kind must fail closed".into()),
276 Err(error) => error,
277 };
278 assert_eq!(error.code(), "invalid_input");
279 assert!(error.to_string().contains("unsupported parser input kind"));
280 Ok(())
281 }
282}