use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelRef {
Path(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataPolicy {
LocalOnly,
Any,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Spec {
pub name: String,
pub description: String,
pub model: ModelRef,
pub data_policy: DataPolicy,
pub read_roots: Vec<PathBuf>,
pub block: PathBuf,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SpecError {
#[error("missing required field `{0}`")]
MissingField(&'static str),
#[error("unknown field `{0}`")]
UnknownField(String),
#[error("malformed spec: {0}")]
Malformed(String),
#[error("unsupported model kind `{0}` (this build supports only `Path`)")]
UnsupportedModel(String),
#[error("unsupported capability `{0}` (this build supports only `Read`)")]
UnsupportedCapability(String),
}
fn quoted(value: &str, field: &str) -> Result<String, SpecError> {
value
.trim()
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.map(str::to_string)
.ok_or_else(|| SpecError::Malformed(format!("field `{field}` must be a quoted string")))
}
fn capabilities(value: &str) -> Result<Vec<PathBuf>, SpecError> {
let inner = value
.trim()
.strip_prefix('[')
.and_then(|v| v.strip_suffix(']'))
.ok_or_else(|| SpecError::Malformed("capabilities must be a `[...]` list".into()))?;
inner
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(|entry| {
let rest = entry.strip_prefix("Read ").ok_or_else(|| {
let kind = entry.split_whitespace().next().unwrap_or(entry);
SpecError::UnsupportedCapability(kind.to_string())
})?;
Ok(PathBuf::from(quoted(rest, "capabilities")?))
})
.collect()
}
pub fn parse_spec(src: &str) -> Result<Spec, SpecError> {
let open = src
.find('{')
.ok_or_else(|| SpecError::Malformed("expected `{`".into()))?;
let close = src
.rfind('}')
.ok_or_else(|| SpecError::Malformed("expected `}`".into()))?;
if close < open {
return Err(SpecError::Malformed("`}` before `{`".into()));
}
let name = src[..open]
.trim()
.strip_prefix("spec")
.and_then(|header| header.split('=').next())
.map(str::trim)
.filter(|name| !name.is_empty())
.ok_or_else(|| SpecError::Malformed("expected `spec <name> = {`".into()))?
.to_string();
let (mut description, mut model, mut data_policy, mut read_roots, mut block) =
(None, None, None, None, None);
for statement in src[open + 1..close].split(';') {
let statement = statement.trim();
if statement.is_empty() {
continue;
}
let (key, value) = statement.split_once('=').ok_or_else(|| {
SpecError::Malformed(format!("expected `key = value` in `{statement}`"))
})?;
let value = value.trim();
match key.trim() {
"description" => description = Some(quoted(value, "description")?),
"block" => block = Some(PathBuf::from(quoted(value, "block")?)),
"capabilities" => read_roots = Some(capabilities(value)?),
"model" => {
let (kind, rest) = value
.split_once(char::is_whitespace)
.ok_or_else(|| SpecError::Malformed("model needs a kind and a value".into()))?;
match kind {
"Path" => model = Some(ModelRef::Path(quoted(rest, "model")?)),
other => return Err(SpecError::UnsupportedModel(other.to_string())),
}
}
"data_policy" => {
data_policy = Some(match value {
"Local_only" => DataPolicy::LocalOnly,
"Any" => DataPolicy::Any,
other => {
return Err(SpecError::Malformed(format!(
"unknown data_policy `{other}`"
)))
}
})
}
other => return Err(SpecError::UnknownField(other.to_string())),
}
}
Ok(Spec {
name,
description: description.ok_or(SpecError::MissingField("description"))?,
model: model.ok_or(SpecError::MissingField("model"))?,
data_policy: data_policy.ok_or(SpecError::MissingField("data_policy"))?,
read_roots: read_roots.ok_or(SpecError::MissingField("capabilities"))?,
block: block.ok_or(SpecError::MissingField("block"))?,
})
}