Skip to main content

open_agent_profile/
parse.rs

1use std::{fs, path::Path};
2
3use serde::{
4    Deserialize, Deserializer,
5    de::{MapAccess, SeqAccess, Visitor},
6};
7use serde_json::{Map, Number, Value};
8use thiserror::Error;
9
10use crate::{Document, object};
11
12/// Supported OAP document encoding.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OapFormat {
15    /// YAML encoding.
16    Yaml,
17    /// JSON encoding.
18    Json,
19    /// YAML frontmatter plus Markdown instructions.
20    Markdown,
21}
22
23impl OapFormat {
24    /// Infers an encoding from `.json` or `.md`, defaulting to YAML.
25    pub fn from_path(path: &Path) -> Self {
26        match path
27            .extension()
28            .and_then(|value| value.to_str())
29            .unwrap_or_default()
30            .to_ascii_lowercase()
31            .as_str()
32        {
33            "json" => Self::Json,
34            "md" => Self::Markdown,
35            _ => Self::Yaml,
36        }
37    }
38}
39
40/// OAP loading or parsing failure.
41#[derive(Debug, Error)]
42#[error("{0}")]
43pub struct ParseError(pub String);
44
45/// Parses an OAP document with duplicate-key rejection.
46pub fn parse(input: &str, format: OapFormat) -> Result<Document, ParseError> {
47    if input.trim().is_empty() {
48        return Err(ParseError("parse error: empty document".into()));
49    }
50    if format == OapFormat::Markdown {
51        let rest = input.strip_prefix("---\n").ok_or_else(|| {
52            ParseError("Markdown profile must begin with YAML frontmatter".into())
53        })?;
54        let (frontmatter, body) = rest
55            .split_once("\n---\n")
56            .ok_or_else(|| ParseError("unterminated Markdown frontmatter".into()))?;
57        let mut document = parse(frontmatter, OapFormat::Yaml)?;
58        let role = document
59            .get_mut("spec")
60            .and_then(Value::as_object_mut)
61            .and_then(|spec| spec.get_mut("role"))
62            .and_then(Value::as_object_mut);
63        if let Some(role) = role {
64            if role.contains_key("instructions") && !body.trim().is_empty() {
65                return Err(ParseError("Markdown encoding supplies spec.role.instructions in both frontmatter and body".into()));
66            }
67            role.insert("instructions".into(), Value::String(body.trim().into()));
68        }
69        return Ok(document);
70    }
71    let value: Value = match format {
72        OapFormat::Json => serde_json::from_str(input)
73            .map_err(|error| ParseError(format!("parse error: {error}")))?,
74        OapFormat::Yaml => serde_yaml_ng::from_str::<UniqueValue>(input)
75            .map(|value| value.0)
76            .map_err(|error| ParseError(format!("parse error: {error}")))?,
77        OapFormat::Markdown => unreachable!(),
78    };
79    value
80        .as_object()
81        .cloned()
82        .ok_or_else(|| ParseError("document root must be an object".into()))
83}
84
85/// Loads an OAP document and infers its encoding from the path.
86pub fn load(path: impl AsRef<Path>) -> Result<Document, ParseError> {
87    let path = path.as_ref();
88    let input = fs::read_to_string(path).map_err(|error| ParseError(error.to_string()))?;
89    parse(&input, OapFormat::from_path(path))
90}
91
92struct UniqueValue(Value);
93impl<'de> Deserialize<'de> for UniqueValue {
94    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
95        struct ValueVisitor;
96        impl<'de> Visitor<'de> for ValueVisitor {
97            type Value = UniqueValue;
98            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
99                formatter.write_str("a JSON-compatible YAML value")
100            }
101            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
102                Ok(UniqueValue(Value::Bool(v)))
103            }
104            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
105                Ok(UniqueValue(Value::Number(v.into())))
106            }
107            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
108                Ok(UniqueValue(Value::Number(v.into())))
109            }
110            fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
111                Number::from_f64(v)
112                    .map(Value::Number)
113                    .map(UniqueValue)
114                    .ok_or_else(|| E::custom("non-finite number"))
115            }
116            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
117                Ok(UniqueValue(Value::String(v.into())))
118            }
119            fn visit_string<E>(self, v: String) -> Result<Self::Value, E> {
120                Ok(UniqueValue(Value::String(v)))
121            }
122            fn visit_none<E>(self) -> Result<Self::Value, E> {
123                Ok(UniqueValue(Value::Null))
124            }
125            fn visit_unit<E>(self) -> Result<Self::Value, E> {
126                Ok(UniqueValue(Value::Null))
127            }
128            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
129                let mut out = vec![];
130                while let Some(v) = seq.next_element::<UniqueValue>()? {
131                    out.push(v.0);
132                }
133                Ok(UniqueValue(Value::Array(out)))
134            }
135            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
136                let mut out = Map::new();
137                while let Some((key, value)) = map.next_entry::<String, UniqueValue>()? {
138                    if out.contains_key(&key) {
139                        return Err(serde::de::Error::custom(format!("duplicate key {key:?}")));
140                    }
141                    out.insert(key, value.0);
142                }
143                Ok(UniqueValue(Value::Object(out)))
144            }
145        }
146        deserializer.deserialize_any(ValueVisitor)
147    }
148}
149
150#[allow(dead_code)]
151fn _object(document: &Document) {
152    let _ = object(document.get("spec"));
153}