use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelRef {
pub provider: String,
pub target: String,
}
impl ModelRef {
pub fn new(provider: impl Into<String>, target: impl Into<String>) -> Self {
Self {
provider: provider.into().to_lowercase(),
target: target.into(),
}
}
}
impl std::fmt::Display for ModelRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.provider, self.target)
}
}
#[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 nodes: crate::graph::NodeGraph,
pub branches: crate::graph::Branches,
}
#[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 capability `{0}` (this build supports only `Read`)")]
UnsupportedCapability(String),
}
use crate::lex::{lex, Tok, Token};
pub fn parse_spec(src: &str) -> Result<Spec, SpecError> {
let tokens = lex(src).map_err(|e| SpecError::Malformed(e.to_string()))?;
Parser {
tokens: &tokens,
at: 0,
}
.spec()
}
struct Parser<'a> {
tokens: &'a [Token],
at: usize,
}
impl<'a> Parser<'a> {
fn peek(&self) -> Option<&'a Tok> {
self.tokens.get(self.at).map(|t| &t.tok)
}
fn here(&self) -> String {
match self.tokens.get(self.at) {
Some(t) => format!("{} at {}", t.tok.describe(), t.span),
None => "end of input".into(),
}
}
fn advance(&mut self) -> Option<&'a Token> {
let t = self.tokens.get(self.at);
if t.is_some() {
self.at += 1;
}
t
}
fn expect(&mut self, want: &Tok) -> Result<(), SpecError> {
match self.peek() {
Some(got) if got == want => {
self.at += 1;
Ok(())
}
_ => Err(SpecError::Malformed(format!(
"expected {}, found {}",
want.describe(),
self.here()
))),
}
}
fn ident(&mut self) -> Result<String, SpecError> {
match self.advance().map(|t| &t.tok) {
Some(Tok::Ident(name)) => Ok(name.clone()),
_ => {
self.at = self.at.saturating_sub(1);
Err(SpecError::Malformed(format!(
"expected a name, found {}",
self.here()
)))
}
}
}
fn string(&mut self, field: &str) -> Result<String, SpecError> {
match self.advance().map(|t| &t.tok) {
Some(Tok::Str(value)) => Ok(value.clone()),
_ => {
self.at = self.at.saturating_sub(1);
Err(SpecError::Malformed(format!(
"field `{field}` must be a quoted string, found {}",
self.here()
)))
}
}
}
fn spec(&mut self) -> Result<Spec, SpecError> {
match self.ident()?.as_str() {
"spec" => {}
other => {
return Err(SpecError::Malformed(format!(
"a spec file starts with `spec`, found `{other}`"
)))
}
}
let name = self.ident()?;
self.expect(&Tok::Equals)?;
self.expect(&Tok::OpenBrace)?;
let (mut description, mut model, mut data_policy, mut read_roots, mut nodes, mut branches) =
(None, None, None, None, None, None);
while self.peek().is_some() && self.peek() != Some(&Tok::CloseBrace) {
let key = self.ident()?;
self.expect(&Tok::Equals)?;
match key.as_str() {
"description" => description = Some(self.string("description")?),
"block" => {
nodes = Some(crate::graph::NodeGraph::single(PathBuf::from(
self.string("block")?,
)))
}
"nodes" => {
let (g, new_at) = crate::graph::GraphParser {
tokens: self.tokens,
at: self.at,
}
.node_graph()?;
self.at = new_at; nodes = Some(g);
}
"branches" => {
let (b, new_at) = crate::graph::GraphParser {
tokens: self.tokens,
at: self.at,
}
.branches()?;
self.at = new_at;
branches = Some(b);
}
"capabilities" => read_roots = Some(self.capabilities()?),
"model" => model = Some(self.model()?),
"data_policy" => {
data_policy = Some(match self.ident()?.as_str() {
"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())),
}
if self.peek() == Some(&Tok::Semicolon) {
self.at += 1;
}
}
self.expect(&Tok::CloseBrace)?;
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"))?,
nodes: nodes.ok_or(SpecError::MissingField("block"))?,
branches: branches.unwrap_or_default(),
})
}
fn model(&mut self) -> Result<ModelRef, SpecError> {
let provider = self.ident()?;
if provider.is_empty() || !provider.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(SpecError::Malformed(format!(
"`{provider}` is not a valid model provider name"
)));
}
Ok(ModelRef::new(provider, self.string("model")?))
}
fn capabilities(&mut self) -> Result<Vec<PathBuf>, SpecError> {
let mut roots = Vec::new();
self.expect(&Tok::OpenBracket)?;
while self.peek() != Some(&Tok::CloseBracket) {
let kind = self.ident()?;
if kind != "Read" {
return Err(SpecError::UnsupportedCapability(kind));
}
roots.push(PathBuf::from(self.string("capabilities")?));
if self.peek() == Some(&Tok::Comma) {
self.at += 1;
} else {
break;
}
}
self.expect(&Tok::CloseBracket)?;
Ok(roots)
}
}