Gleam-inspired typed configuration language (POC)
1import gleam/option.{None, Some}
2import gleam/string
3import gleeunit
4import glint
5import glint/check
6import glint/lsp/diagnostics
7import glint/lsp/hover
8import glint/pipeline
9import glint/position
10import glint/value
11
12pub fn main() -> Nil {
13 gleeunit.main()
14}
15
16pub fn hello_loads_test() {
17 let source =
18 "
19type Mode {
20 Dev
21 Prod
22}
23
24type Config {
25 Config(
26 name: String,
27 mode: Mode,
28 port: Int,
29 )
30}
31
32pub let config = Config(
33 name: \"hello\",
34 mode: Dev,
35 port: 3000,
36)
37"
38
39 let assert Ok(checked) = pipeline.load(source)
40 let assert "Config" = check.type_to_string(checked.config_type)
41 let assert True =
42 string.contains(value.to_glint(checked.config), "\"hello\"")
43 let assert True = string.contains(value.to_json(checked.config), "hello")
44}
45
46pub fn app_example_test() {
47 let source =
48 "
49type LogLevel {
50 Debug
51 Info
52}
53
54type Database {
55 Database(
56 host: String,
57 port: Int,
58 password: Option(String),
59 )
60}
61
62type Config {
63 Config(
64 log_level: LogLevel,
65 database: Database,
66 origins: List(String),
67 )
68}
69
70let database = Database(
71 host: \"localhost\",
72 port: 5432,
73 password: None,
74)
75
76pub let config = Config(
77 log_level: Info,
78 database: database,
79 origins: [\"https://example.com\"],
80)
81"
82
83 let assert Ok(checked) = glint.load(source)
84 let assert value.VVariant("Config", fields) = checked.config
85 let assert True = list_has_tag(fields, "log_level")
86}
87
88pub fn type_mismatch_test() {
89 let source =
90 "
91type Config {
92 Config(port: Int)
93}
94
95pub let config = Config(port: \"nope\")
96"
97 let assert Error(msg) = glint.load(source)
98 let assert True = string.contains(msg, "type mismatch")
99}
100
101pub fn missing_config_test() {
102 let source =
103 "
104type Mode {
105 Dev
106}
107
108let mode = Dev
109"
110 let assert Error(msg) = glint.load(source)
111 let assert True = string.contains(msg, "pub let config")
112}
113
114pub fn unknown_constructor_test() {
115 let source =
116 "
117type Config {
118 Config(name: String)
119}
120
121pub let config = Config(name: \"x\", extra: 1)
122"
123 let assert Error(msg) = glint.load(source)
124 let assert True = string.contains(msg, "unknown field")
125}
126
127pub fn comments_and_trailing_commas_test() {
128 let source =
129 "
130// leading comment
131type Mode {
132 Dev
133 Prod
134}
135
136type Config {
137 Config(
138 mode: Mode,
139 port: Int,
140 )
141}
142
143pub let config = Config(
144 mode: Dev,
145 port: 1_000,
146)
147"
148 let assert Ok(_) = glint.load(source)
149}
150
151fn list_has_tag(fields: List(#(String, value.Value)), label: String) -> Bool {
152 case fields {
153 [] -> False
154 [#(l, _), ..rest] ->
155 case l == label {
156 True -> True
157 False -> list_has_tag(rest, label)
158 }
159 }
160}
161
162pub fn host_accessors_test() {
163 let source =
164 "
165type Mode {
166 Dev
167 Prod
168}
169
170type Server {
171 Server(host: String, port: Int)
172}
173
174type Config {
175 Config(
176 name: String,
177 mode: Mode,
178 server: Server,
179 )
180}
181
182pub let config = Config(
183 name: \"hello\",
184 mode: Dev,
185 server: Server(host: \"localhost\", port: 3000),
186)
187"
188 let assert Ok(checked) = glint.load(source)
189 let cfg = checked.config
190 let assert "hello" = glint.string(cfg, "name")
191 let assert "Dev" = glint.unit(cfg, "mode")
192 let assert 3000 = glint.int(cfg, "server.port")
193 let assert "localhost" = glint.string(cfg, "server.host")
194}
195
196// ── Position / diagnostics ──────────────────────────────────────────
197
198pub fn offset_to_position_test() {
199 let source = "abc\ndef"
200 let assert position.Position(0, 0) = position.offset_to_position(source, 0)
201 let assert position.Position(0, 2) = position.offset_to_position(source, 2)
202 let assert position.Position(1, 0) = position.offset_to_position(source, 4)
203 let assert position.Position(1, 2) = position.offset_to_position(source, 6)
204}
205
206pub fn range_from_offsets_test() {
207 let source = "let x = 1"
208 let range = position.range_from_offsets(source, 0, 3)
209 let assert position.Position(0, 0) = range.start
210 let assert position.Position(0, 3) = range.end
211}
212
213pub fn valid_source_no_diagnostics_test() {
214 let source =
215 "
216type Config {
217 Config(port: Int)
218}
219
220pub let config = Config(port: 1)
221"
222 let assert [] = diagnostics.analyse(source)
223}
224
225pub fn lex_error_has_range_test() {
226 // `@` is not a valid token
227 let source = "let x = @"
228 let diags = diagnostics.analyse(source)
229 let assert [d] = diags
230 let assert True = string.contains(d.message, "lex error")
231 // Range should not be the default empty only — character should land near `@`
232 let assert True = d.range.start.line >= 0
233 let assert True = d.range.start.character >= 0
234}
235
236pub fn parse_error_has_range_test() {
237 // Missing `=` after name
238 let source = "let x 1"
239 let diags = diagnostics.analyse(source)
240 let assert [d] = diags
241 let assert True = string.contains(d.message, "parse error")
242 let assert True =
243 d.range.start.character > 0 || d.range.end.character > d.range.start.character
244}
245
246pub fn check_error_diagnostic_test() {
247 let source =
248 "
249type Config {
250 Config(port: Int)
251}
252
253pub let config = Config(port: \"nope\")
254"
255 let diags = diagnostics.analyse(source)
256 let assert [d] = diags
257 let assert True = string.contains(d.message, "type error")
258}
259
260// ── Hover ───────────────────────────────────────────────────────────
261
262pub fn position_to_offset_test() {
263 let source = "abc\ndef"
264 let assert 0 =
265 position.position_to_offset(source, position.Position(0, 0))
266 let assert 2 =
267 position.position_to_offset(source, position.Position(0, 2))
268 let assert 4 =
269 position.position_to_offset(source, position.Position(1, 0))
270 let assert 6 =
271 position.position_to_offset(source, position.Position(1, 2))
272 // Past end of line clamps to before newline
273 let assert 3 =
274 position.position_to_offset(source, position.Position(0, 99))
275}
276
277pub fn hover_builtin_test() {
278 let source = "type Config { Config(name: String) }\npub let config = Config(name: \"x\")"
279 // Hover on `String` in the field type
280 let assert Some(h) = hover.at(source, 0, 30)
281 let assert True = string.contains(h.contents.value, "String")
282 let assert True = string.contains(h.contents.value, "Built-in")
283}
284
285pub fn hover_variable_test() {
286 let source =
287 "type Mode { Dev }\ntype Config { Config(mode: Mode) }\npub let config = Config(mode: Dev)"
288 // Find line with `config` binding — line 2 (0-based), character of `c` in config
289 let assert Some(h) = hover.at(source, 2, 8)
290 let assert True = string.contains(h.contents.value, "config")
291 let assert True = string.contains(h.contents.value, "Config")
292}
293
294pub fn hover_constructor_test() {
295 // `Dev` starts at character 30 on line 2:
296 // "pub let config = Config(mode: Dev)"
297 let source =
298 "type Mode { Dev Prod }\ntype Config { Config(mode: Mode) }\npub let config = Config(mode: Dev)"
299 let assert Some(h) = hover.at(source, 2, 30)
300 let assert True = string.contains(h.contents.value, "Dev")
301 let assert True = string.contains(h.contents.value, "Mode")
302}
303
304pub fn hover_type_test() {
305 let source =
306 "type Mode { Dev }\ntype Config { Config(mode: Mode) }\npub let config = Config(mode: Dev)"
307 // `Mode` in `type Mode`
308 let assert Some(h) = hover.at(source, 0, 5)
309 let assert True = string.contains(h.contents.value, "type")
310 let assert True = string.contains(h.contents.value, "Mode")
311}
312
313pub fn hover_whitespace_none_test() {
314 let source = "type Mode { Dev }\n"
315 let assert None = hover.at(source, 0, 4)
316}