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