Skip to main content

runx_runtime/
parser_eval.rs

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}