1use std::path::PathBuf;
31use thiserror::Error;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ModelRef {
36 Path(String),
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DataPolicy {
49 LocalOnly,
51 Any,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct Spec {
58 pub name: String,
60 pub description: String,
64 pub model: ModelRef,
66 pub data_policy: DataPolicy,
68 pub read_roots: Vec<PathBuf>,
70 pub block: PathBuf,
72}
73
74#[derive(Debug, Error, PartialEq, Eq)]
76pub enum SpecError {
77 #[error("missing required field `{0}`")]
79 MissingField(&'static str),
80 #[error("unknown field `{0}`")]
82 UnknownField(String),
83 #[error("malformed spec: {0}")]
85 Malformed(String),
86 #[error("unsupported model kind `{0}` (this build supports only `Path`)")]
88 UnsupportedModel(String),
89 #[error("unsupported capability `{0}` (this build supports only `Read`)")]
91 UnsupportedCapability(String),
92}
93
94fn quoted(value: &str, field: &str) -> Result<String, SpecError> {
96 value
97 .trim()
98 .strip_prefix('"')
99 .and_then(|v| v.strip_suffix('"'))
100 .map(str::to_string)
101 .ok_or_else(|| SpecError::Malformed(format!("field `{field}` must be a quoted string")))
102}
103
104fn capabilities(value: &str) -> Result<Vec<PathBuf>, SpecError> {
106 let inner = value
107 .trim()
108 .strip_prefix('[')
109 .and_then(|v| v.strip_suffix(']'))
110 .ok_or_else(|| SpecError::Malformed("capabilities must be a `[...]` list".into()))?;
111
112 inner
113 .split(',')
114 .map(str::trim)
115 .filter(|entry| !entry.is_empty())
116 .map(|entry| {
117 let rest = entry.strip_prefix("Read ").ok_or_else(|| {
118 let kind = entry.split_whitespace().next().unwrap_or(entry);
122 SpecError::UnsupportedCapability(kind.to_string())
123 })?;
124 Ok(PathBuf::from(quoted(rest, "capabilities")?))
125 })
126 .collect()
127}
128
129pub fn parse_spec(src: &str) -> Result<Spec, SpecError> {
131 let open = src
132 .find('{')
133 .ok_or_else(|| SpecError::Malformed("expected `{`".into()))?;
134 let close = src
135 .rfind('}')
136 .ok_or_else(|| SpecError::Malformed("expected `}`".into()))?;
137 if close < open {
138 return Err(SpecError::Malformed("`}` before `{`".into()));
139 }
140
141 let name = src[..open]
142 .trim()
143 .strip_prefix("spec")
144 .and_then(|header| header.split('=').next())
145 .map(str::trim)
146 .filter(|name| !name.is_empty())
147 .ok_or_else(|| SpecError::Malformed("expected `spec <name> = {`".into()))?
148 .to_string();
149
150 let (mut description, mut model, mut data_policy, mut read_roots, mut block) =
151 (None, None, None, None, None);
152
153 for statement in src[open + 1..close].split(';') {
154 let statement = statement.trim();
155 if statement.is_empty() {
156 continue;
157 }
158
159 let (key, value) = statement.split_once('=').ok_or_else(|| {
160 SpecError::Malformed(format!("expected `key = value` in `{statement}`"))
161 })?;
162 let value = value.trim();
163
164 match key.trim() {
165 "description" => description = Some(quoted(value, "description")?),
166 "block" => block = Some(PathBuf::from(quoted(value, "block")?)),
167 "capabilities" => read_roots = Some(capabilities(value)?),
168 "model" => {
169 let (kind, rest) = value
170 .split_once(char::is_whitespace)
171 .ok_or_else(|| SpecError::Malformed("model needs a kind and a value".into()))?;
172 match kind {
173 "Path" => model = Some(ModelRef::Path(quoted(rest, "model")?)),
174 other => return Err(SpecError::UnsupportedModel(other.to_string())),
175 }
176 }
177 "data_policy" => {
178 data_policy = Some(match value {
179 "Local_only" => DataPolicy::LocalOnly,
180 "Any" => DataPolicy::Any,
181 other => {
182 return Err(SpecError::Malformed(format!(
183 "unknown data_policy `{other}`"
184 )))
185 }
186 })
187 }
188 other => return Err(SpecError::UnknownField(other.to_string())),
189 }
190 }
191
192 Ok(Spec {
193 name,
194 description: description.ok_or(SpecError::MissingField("description"))?,
195 model: model.ok_or(SpecError::MissingField("model"))?,
196 data_policy: data_policy.ok_or(SpecError::MissingField("data_policy"))?,
197 read_roots: read_roots.ok_or(SpecError::MissingField("capabilities"))?,
198 block: block.ok_or(SpecError::MissingField("block"))?,
199 })
200}