Skip to main content

ought_spec/parser/
mod.rs

1//! Public parsing interface for ought specs.
2//!
3//! The [`Parser`] trait is the abstraction boundary; [`OughtMdParser`] is
4//! the canonical implementation for `.ought.md` files. The state-machine
5//! driving the actual parse, plus all pure helpers, live in sibling
6//! submodules below this one.
7
8use std::path::Path;
9
10use crate::types::{ParseError, Spec};
11
12mod clauses;
13mod ids;
14mod keywords;
15mod metadata;
16mod state;
17
18/// The public interface for parsing spec files into the ought IR.
19///
20/// Mirrors `ought_run::Runner`: one trait, one concrete implementation today
21/// (`OughtMdParser`), and room to add more spec formats without breaking
22/// callers. Most consumers should take `&dyn Parser` (or `impl Parser`)
23/// rather than naming a concrete type.
24pub trait Parser: Send + Sync {
25    /// Parse a spec file from disk. The default implementation reads the
26    /// file and delegates to [`Parser::parse_string`], so format-specific
27    /// parsers usually only need to implement `parse_string`.
28    fn parse_file(&self, path: &Path) -> Result<Spec, Vec<ParseError>> {
29        let content = std::fs::read_to_string(path).map_err(|e| {
30            vec![ParseError {
31                file: path.to_path_buf(),
32                line: 0,
33                message: format!("failed to read file: {}", e),
34            }]
35        })?;
36        self.parse_string(&content, path)
37    }
38
39    /// Parse a spec from an in-memory string, using `path` only as the
40    /// source-location label for error messages and source locations.
41    fn parse_string(&self, content: &str, path: &Path) -> Result<Spec, Vec<ParseError>>;
42
43    /// Short, stable name for this parser (e.g. `"ought.md"`). Used for
44    /// diagnostics and, eventually, format dispatch.
45    fn name(&self) -> &str;
46}
47
48/// Canonical parser for `.ought.md` files: CommonMark markdown with bold
49/// deontic keywords (`**MUST**`, `**SHOULD**`, …), GIVEN nesting,
50/// OTHERWISE chains, and MUST BY duration literals.
51///
52/// Pure Rust, no LLM dependency.
53#[derive(Debug, Default, Clone, Copy)]
54pub struct OughtMdParser;
55
56impl Parser for OughtMdParser {
57    fn parse_string(&self, content: &str, path: &Path) -> Result<Spec, Vec<ParseError>> {
58        state::parse_string(content, path)
59    }
60
61    fn name(&self) -> &str {
62        "ought.md"
63    }
64}