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    /// Exact filenames this decree handles (e.g., `["Gemfile", "Rakefile"]`)
41    #[serde(default)]
42    pub supported_filenames: Vec<String>,
43    /// Filenames to claim but NOT lint (lock files, generated files)
44    /// Decree owns these to prevent other decrees from touching them.
45    #[serde(default)]
46    pub skip_filenames: Vec<String>,
47    /// Capabilities this decree provides
48    pub capabilities: Vec<Capability>,
49}
50
51impl DecreeMetadata {
52    /// Check if this decree has a specific capability.
53    #[must_use]
54    pub fn has_capability(&self, cap: Capability) -> bool {
55        self.capabilities.contains(&cap)
56    }
57
58    /// Parse semver version string.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the version string is not in the format "major.minor.patch"
63    /// or if any component cannot be parsed as a u32.
64    pub fn parse_version(version: &str) -> Result<(u32, u32, u32), String> {
65        let parts: Vec<&str> = version.split('.').collect();
66        if parts.len() != 3 {
67            return Err(format!("invalid version format: {version}"));
68        }
69        let major = parts[0]
70            .parse()
71            .map_err(|_| format!("invalid major: {}", parts[0]))?;
72        let minor = parts[1]
73            .parse()
74            .map_err(|_| format!("invalid minor: {}", parts[1]))?;
75        let patch = parts[2]
76            .parse()
77            .map_err(|_| format!("invalid patch: {}", parts[2]))?;
78        Ok((major, minor, patch))
79    }
80
81    /// Check if this decree's ABI version is compatible with host ABI version.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the ABI versions are incompatible or if version parsing fails.
86    pub fn validate_abi(&self, host_abi_version: &str) -> Result<(), String> {
87        let (host_maj, host_min, _) = Self::parse_version(host_abi_version)?;
88        let (decree_maj, decree_min, _) = Self::parse_version(&self.abi_version)?;
89
90        // Pre-1.0: exact major.minor match required
91        if host_maj == 0 {
92            if host_maj == decree_maj && host_min == decree_min {
93                return Ok(());
94            }
95            return Err(format!(
96                "ABI version mismatch: host {}, decree {}",
97                host_abi_version, self.abi_version
98            ));
99        }
100
101        // Post-1.0: major must match, decree minor ≤ host minor
102        if host_maj == decree_maj && decree_min <= host_min {
103            return Ok(());
104        }
105
106        Err(format!(
107            "ABI version incompatible: host {}, decree {}",
108            host_abi_version, self.abi_version
109        ))
110    }
111}
112
113/// Byte offsets into the source file (half-open range).
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115pub struct Span {
116    pub start: usize,
117    pub end: usize,
118}
119
120impl Span {
121    /// Create a new span with the given start and end offsets.
122    #[must_use]
123    pub const fn new(start: usize, end: usize) -> Self {
124        Self { start, end }
125    }
126
127    /// Check if this span is empty (start >= end).
128    #[must_use]
129    pub const fn is_empty(&self) -> bool {
130        self.start >= self.end
131    }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Diagnostic {
136    /// Rule identifier, e.g. "ruby/trailing-whitespace".
137    pub rule: String,
138    pub message: String,
139    pub span: Span,
140    /// true = Dictator enforced (auto-fixed), false = you must comply
141    pub enforced: bool,
142}
143
144pub type Diagnostics = Vec<Diagnostic>;
145
146/// Trait all Dictator decrees must implement. Designed to be usable from WASM by
147/// exporting a thin C-ABI shim that instantiates a concrete implementor.
148pub trait Decree: Send + Sync {
149    /// Human-friendly decree name, e.g. "ruby".
150    #[must_use]
151    fn name(&self) -> &str;
152
153    /// Lint a single file, returning diagnostics. `path` is UTF-8.
154    fn lint(&self, path: &str, source: &str) -> Diagnostics;
155
156    /// Metadata for versioning and capabilities.
157    #[must_use]
158    fn metadata(&self) -> DecreeMetadata;
159
160    /// Create rule identifier: `{decree}/{rule}` - DRY helper.
161    #[must_use]
162    fn rule(&self, rule_name: &str) -> String {
163        format!("{}/{}", self.name(), rule_name)
164    }
165}
166
167/// Boxed decree for dynamic dispatch.
168pub type BoxDecree = Box<dyn Decree>;
169
170/// Function exported by WASM decrees to construct an instance.
171pub type DecreeFactory = fn() -> BoxDecree;
172
173/// Export name expected in decrees for the factory symbol.
174pub const DECREE_FACTORY_EXPORT: &str = "dictator_create_decree";