dictator_decree_abi/
lib.rs

1#![warn(rust_2024_compatibility, clippy::all)]
2
3use serde::{Deserialize, Serialize};
4
5/// ABI version for decree compatibility checking.
6///
7/// Bumped when Plugin trait or core types change.
8/// Pre-1.0: exact major.minor match required (0.1.x ↔ 0.1.y ✓, 0.1.x ↔ 0.2.y ✗)
9/// Post-1.0: major must match, decree minor ≤ host minor
10pub const ABI_VERSION: &str = "0.1.0";
11
12/// Capability flags for decrees
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum Capability {
15    /// Basic linting (always required)
16    Lint,
17    /// Can auto-fix issues
18    AutoFix,
19    /// Supports incremental/streaming linting
20    Streaming,
21    /// Accepts config at lint-time
22    RuntimeConfig,
23    /// Returns enhanced diagnostics (quickfixes, etc.)
24    RichDiagnostics,
25}
26
27/// Metadata for decree versioning and capabilities
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DecreeMetadata {
30    /// ABI version this decree was built against
31    pub abi_version: String,
32    /// Decree's own version (e.g., "0.60.0" for kjr)
33    pub decree_version: String,
34    /// Human-readable description
35    pub description: String,
36    /// Decree authors (from workspace, optional)
37    pub dectauthors: Option<String>,
38    /// File extensions this decree handles (e.g., `["rb", "rake"]`)
39    pub supported_extensions: Vec<String>,
40    /// Capabilities this decree provides
41    pub capabilities: Vec<Capability>,
42}
43
44impl DecreeMetadata {
45    /// Check if this decree has a specific capability.
46    #[must_use]
47    pub fn has_capability(&self, cap: Capability) -> bool {
48        self.capabilities.contains(&cap)
49    }
50
51    /// Parse semver version string.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the version string is not in the format "major.minor.patch"
56    /// or if any component cannot be parsed as a u32.
57    pub fn parse_version(version: &str) -> Result<(u32, u32, u32), String> {
58        let parts: Vec<&str> = version.split('.').collect();
59        if parts.len() != 3 {
60            return Err(format!("invalid version format: {version}"));
61        }
62        let major = parts[0]
63            .parse()
64            .map_err(|_| format!("invalid major: {}", parts[0]))?;
65        let minor = parts[1]
66            .parse()
67            .map_err(|_| format!("invalid minor: {}", parts[1]))?;
68        let patch = parts[2]
69            .parse()
70            .map_err(|_| format!("invalid patch: {}", parts[2]))?;
71        Ok((major, minor, patch))
72    }
73
74    /// Check if this decree's ABI version is compatible with host ABI version.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the ABI versions are incompatible or if version parsing fails.
79    pub fn validate_abi(&self, host_abi_version: &str) -> Result<(), String> {
80        let (host_maj, host_min, _) = Self::parse_version(host_abi_version)?;
81        let (decree_maj, decree_min, _) = Self::parse_version(&self.abi_version)?;
82
83        // Pre-1.0: exact major.minor match required
84        if host_maj == 0 {
85            if host_maj == decree_maj && host_min == decree_min {
86                return Ok(());
87            }
88            return Err(format!(
89                "ABI version mismatch: host {}, decree {}",
90                host_abi_version, self.abi_version
91            ));
92        }
93
94        // Post-1.0: major must match, decree minor ≤ host minor
95        if host_maj == decree_maj && decree_min <= host_min {
96            return Ok(());
97        }
98
99        Err(format!(
100            "ABI version incompatible: host {}, decree {}",
101            host_abi_version, self.abi_version
102        ))
103    }
104}
105
106/// Byte offsets into the source file (half-open range).
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub struct Span {
109    pub start: usize,
110    pub end: usize,
111}
112
113impl Span {
114    /// Create a new span with the given start and end offsets.
115    #[must_use]
116    pub const fn new(start: usize, end: usize) -> Self {
117        Self { start, end }
118    }
119
120    /// Check if this span is empty (start >= end).
121    #[must_use]
122    pub const fn is_empty(&self) -> bool {
123        self.start >= self.end
124    }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct Diagnostic {
129    /// Rule identifier, e.g. "ruby/trailing-whitespace".
130    pub rule: String,
131    pub message: String,
132    pub span: Span,
133    /// true = Dictator enforced (auto-fixed), false = you must comply
134    pub enforced: bool,
135}
136
137pub type Diagnostics = Vec<Diagnostic>;
138
139/// Trait all Dictator decrees must implement. Designed to be usable from WASM by
140/// exporting a thin C-ABI shim that instantiates a concrete implementor.
141pub trait Decree: Send + Sync {
142    /// Human-friendly decree name, e.g. "ruby".
143    #[must_use]
144    fn name(&self) -> &str;
145
146    /// Lint a single file, returning diagnostics. `path` is UTF-8.
147    fn lint(&self, path: &str, source: &str) -> Diagnostics;
148
149    /// Metadata for versioning and capabilities.
150    #[must_use]
151    fn metadata(&self) -> DecreeMetadata;
152
153    /// Create rule identifier: `{decree}/{rule}` - DRY helper.
154    #[must_use]
155    fn rule(&self, rule_name: &str) -> String {
156        format!("{}/{}", self.name(), rule_name)
157    }
158}
159
160/// Boxed decree for dynamic dispatch.
161pub type BoxDecree = Box<dyn Decree>;
162
163/// Function exported by WASM decrees to construct an instance.
164pub type DecreeFactory = fn() -> BoxDecree;
165
166/// Export name expected in decrees for the factory symbol.
167pub const DECREE_FACTORY_EXPORT: &str = "dictator_create_decree";