Skip to main content

shape_lsp/toml_support/
schema.rs

1//! Static schema definitions for shape.toml.
2//!
3//! Drives completions, hover, and diagnostics by describing every valid
4//! section, key, type, and default in the shape.toml format.
5
6/// The type a TOML key expects.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ValueType {
9    Str,
10    Integer,
11    Bool,
12    ArrayOfStrings,
13    /// Inline table or complex value (e.g. dependency spec)
14    Table,
15}
16
17impl ValueType {
18    pub fn display_name(self) -> &'static str {
19        match self {
20            ValueType::Str => "string",
21            ValueType::Integer => "integer",
22            ValueType::Bool => "boolean",
23            ValueType::ArrayOfStrings => "array of strings",
24            ValueType::Table => "table",
25        }
26    }
27}
28
29/// A key within a TOML section.
30#[derive(Debug, Clone)]
31pub struct KeyDef {
32    pub name: &'static str,
33    pub value_type: ValueType,
34    pub required: bool,
35    pub description: &'static str,
36    /// Optional list of known valid values for completion.
37    pub known_values: &'static [&'static str],
38}
39
40/// A top-level section (e.g. `[project]`) in shape.toml.
41#[derive(Debug, Clone)]
42pub struct SectionDef {
43    pub name: &'static str,
44    pub description: &'static str,
45    /// Whether this is an array-of-tables section (e.g. `[[extensions]]`).
46    pub is_array_table: bool,
47    /// Whether this section holds arbitrary key-value pairs (like [dependencies]).
48    pub is_free_form: bool,
49    pub keys: &'static [KeyDef],
50}
51
52/// All known sections in shape.toml.
53pub static SECTIONS: &[SectionDef] = &[
54    SectionDef {
55        name: "project",
56        description: "Project metadata and configuration.",
57        is_array_table: false,
58        is_free_form: false,
59        keys: &[
60            KeyDef {
61                name: "name",
62                value_type: ValueType::Str,
63                required: true,
64                description: "The project name. Must be non-empty.",
65                known_values: &[],
66            },
67            KeyDef {
68                name: "version",
69                value_type: ValueType::Str,
70                required: true,
71                description: "The project version (semver recommended, e.g. \"0.1.0\").",
72                known_values: &[],
73            },
74            KeyDef {
75                name: "entry",
76                value_type: ValueType::Str,
77                required: false,
78                description: "Entry script for `shape` with no args (project mode). E.g. \"src/main.shape\".",
79                known_values: &[],
80            },
81            KeyDef {
82                name: "authors",
83                value_type: ValueType::ArrayOfStrings,
84                required: false,
85                description: "List of project authors.",
86                known_values: &[],
87            },
88            KeyDef {
89                name: "shape-version",
90                value_type: ValueType::Str,
91                required: false,
92                description: "Required Shape language version (e.g. \">=0.5.0\").",
93                known_values: &[],
94            },
95            KeyDef {
96                name: "license",
97                value_type: ValueType::Str,
98                required: false,
99                description: "SPDX license identifier (e.g. \"MIT\", \"Apache-2.0\").",
100                known_values: &[
101                    "MIT",
102                    "Apache-2.0",
103                    "GPL-3.0",
104                    "BSD-3-Clause",
105                    "ISC",
106                    "Unlicense",
107                ],
108            },
109            KeyDef {
110                name: "repository",
111                value_type: ValueType::Str,
112                required: false,
113                description: "URL of the project's source repository.",
114                known_values: &[],
115            },
116        ],
117    },
118    SectionDef {
119        name: "modules",
120        description: "Module resolution configuration.",
121        is_array_table: false,
122        is_free_form: false,
123        keys: &[KeyDef {
124            name: "paths",
125            value_type: ValueType::ArrayOfStrings,
126            required: false,
127            description: "Additional directories to search for modules (relative to project root).",
128            known_values: &[],
129        }],
130    },
131    SectionDef {
132        name: "dependencies",
133        description: "Project dependencies. Each key is a package name, value is a version string or detailed table.",
134        is_array_table: false,
135        is_free_form: true,
136        keys: &[],
137    },
138    SectionDef {
139        name: "dev-dependencies",
140        description: "Development-only dependencies (not included in production builds).",
141        is_array_table: false,
142        is_free_form: true,
143        keys: &[],
144    },
145    SectionDef {
146        name: "build",
147        description: "Build configuration.",
148        is_array_table: false,
149        is_free_form: false,
150        keys: &[
151            KeyDef {
152                name: "target",
153                value_type: ValueType::Str,
154                required: false,
155                description: "Build target: \"bytecode\" or \"native\".",
156                known_values: &["bytecode", "native"],
157            },
158            KeyDef {
159                name: "opt_level",
160                value_type: ValueType::Integer,
161                required: false,
162                description: "Optimization level (0-3). Higher is more optimized.",
163                known_values: &["0", "1", "2", "3"],
164            },
165            KeyDef {
166                name: "output",
167                value_type: ValueType::Str,
168                required: false,
169                description: "Output directory for build artifacts.",
170                known_values: &[],
171            },
172        ],
173    },
174    SectionDef {
175        name: "extensions",
176        description: "Extension module libraries to load. Each `[[extensions]]` entry defines one module.",
177        is_array_table: true,
178        is_free_form: false,
179        keys: &[
180            KeyDef {
181                name: "name",
182                value_type: ValueType::Str,
183                required: true,
184                description: "Name of the extension module.",
185                known_values: &[],
186            },
187            KeyDef {
188                name: "path",
189                value_type: ValueType::Str,
190                required: true,
191                description: "Path to the shared library (.so/.dylib/.dll).",
192                known_values: &[],
193            },
194            KeyDef {
195                name: "config",
196                value_type: ValueType::Table,
197                required: false,
198                description: "Module-specific configuration table.",
199                known_values: &[],
200            },
201        ],
202    },
203];
204
205/// Look up a section definition by name (case-sensitive).
206pub fn find_section(name: &str) -> Option<&'static SectionDef> {
207    SECTIONS.iter().find(|s| s.name == name)
208}
209
210/// Look up a key definition within a section.
211pub fn find_key(section_name: &str, key_name: &str) -> Option<&'static KeyDef> {
212    find_section(section_name).and_then(|s| s.keys.iter().find(|k| k.name == key_name))
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_find_section() {
221        assert!(find_section("project").is_some());
222        assert!(find_section("dependencies").is_some());
223        assert!(find_section("nonexistent").is_none());
224    }
225
226    #[test]
227    fn test_find_key() {
228        let key = find_key("project", "name");
229        assert!(key.is_some());
230        let key = key.unwrap();
231        assert!(key.required);
232        assert_eq!(key.value_type, ValueType::Str);
233    }
234
235    #[test]
236    fn test_all_sections_present() {
237        let names: Vec<&str> = SECTIONS.iter().map(|s| s.name).collect();
238        assert!(names.contains(&"project"));
239        assert!(names.contains(&"modules"));
240        assert!(names.contains(&"dependencies"));
241        assert!(names.contains(&"dev-dependencies"));
242        assert!(names.contains(&"build"));
243        assert!(names.contains(&"extensions"));
244    }
245
246    #[test]
247    fn test_extensions_is_array_table() {
248        let ext = find_section("extensions").unwrap();
249        assert!(ext.is_array_table);
250    }
251
252    #[test]
253    fn test_dependencies_is_free_form() {
254        let deps = find_section("dependencies").unwrap();
255        assert!(deps.is_free_form);
256        let dev_deps = find_section("dev-dependencies").unwrap();
257        assert!(dev_deps.is_free_form);
258    }
259}