Gleam-inspired typed configuration language (POC)
1//// Glint — a Gleam-inspired configuration language (POC).
2////
3//// CLI:
4//// gleam run -- check <file.glint>
5//// gleam run -- dump <file.glint>
6//// gleam run -- dump <file.glint> --json
7////
8//// Host app (no ceremony):
9//// let c = glint.file("config.glint")
10//// glint.int(c, "port")
11//// glint.string(c, "server.host")
12
13import argv
14import gleam/io
15import gleam/list
16import gleam/result
17import gleam/string
18import glint/check
19import glint/lsp as glint_lsp
20import glint/pipeline
21import glint/value.{type Value}
22import simplifile
23
24// ── Host API ────────────────────────────────────────────────────────
25// Load once; read fields by path. Wrong field = bug (panic), not a
26// recoverable Result chain — Glint already typechecked the file.
27
28/// Load a `.glint` file and return its root `config` value.
29/// Panics with a readable message if the file is missing or invalid.
30pub fn file(path: String) -> Value {
31 case read(path) {
32 Ok(v) -> v
33 Error(e) -> panic as e
34 }
35}
36
37/// Like `file`, but as a `Result` when you want to handle errors.
38pub fn read(path: String) -> Result(Value, String) {
39 use checked <- result.try(load_file(path))
40 Ok(checked.config)
41}
42
43/// Nested field access via dots: `"port"`, `"server.host"`.
44pub fn at(cfg: Value, path: String) -> Value {
45 dig(cfg, path_parts(path))
46}
47
48pub fn string(cfg: Value, path: String) -> String {
49 expect(path, value.as_string(at(cfg, path)))
50}
51
52pub fn int(cfg: Value, path: String) -> Int {
53 expect(path, value.as_int(at(cfg, path)))
54}
55
56pub fn bool(cfg: Value, path: String) -> Bool {
57 expect(path, value.as_bool(at(cfg, path)))
58}
59
60pub fn float(cfg: Value, path: String) -> Float {
61 expect(path, value.as_float(at(cfg, path)))
62}
63
64/// Unit variant tag at path (`mode` → `"Dev"`).
65pub fn unit(cfg: Value, path: String) -> String {
66 expect(path, value.as_unit(at(cfg, path)))
67}
68
69pub fn list(cfg: Value, path: String) -> List(Value) {
70 expect(path, value.as_list(at(cfg, path)))
71}
72
73// ── CLI ─────────────────────────────────────────────────────────────
74
75pub fn main() -> Nil {
76 case argv.load().arguments {
77 ["check", path] -> run_check(path)
78 ["dump", path] -> run_dump(path, False)
79 ["dump", path, "--json"] -> run_dump(path, True)
80 ["lsp"] -> glint_lsp.main()
81 ["help"] | ["--help"] | ["-h"] -> print_usage()
82 [] -> print_usage()
83 args -> {
84 io.println("unknown arguments: " <> string.join(args, " "))
85 print_usage()
86 }
87 }
88}
89
90fn print_usage() -> Nil {
91 io.println(
92 "Glint POC — typed config language
93
94Usage:
95 gleam run -- check <file.glint>
96 gleam run -- dump <file.glint>
97 gleam run -- dump <file.glint> --json
98 gleam run -- lsp
99",
100 )
101}
102
103fn run_check(path: String) -> Nil {
104 case load_file(path) {
105 Error(msg) -> io.println_error(msg)
106 Ok(checked) ->
107 io.println(
108 "ok config: " <> check.type_to_string(checked.config_type),
109 )
110 }
111}
112
113fn run_dump(path: String, as_json: Bool) -> Nil {
114 case load_file(path) {
115 Error(msg) -> io.println_error(msg)
116 Ok(checked) ->
117 case as_json {
118 True -> io.println(value.to_json(checked.config))
119 False -> io.println(value.to_glint(checked.config))
120 }
121 }
122}
123
124// ── Lower-level (tests, tooling) ────────────────────────────────────
125
126/// Load and typecheck a source string.
127pub fn load(source: String) -> Result(check.Checked, String) {
128 case pipeline.load(source) {
129 Error(e) -> Error(pipeline.error_to_string(e))
130 Ok(checked) -> Ok(checked)
131 }
132}
133
134/// Read + load a file (keeps type info). Prefer `file` / `read` in apps.
135pub fn load_file(path: String) -> Result(check.Checked, String) {
136 case simplifile.read(path) {
137 Error(e) ->
138 Error(
139 "could not read "
140 <> path
141 <> ": "
142 <> simplifile.describe_error(e),
143 )
144 Ok(source) -> load(source)
145 }
146}
147
148pub fn dump(source: String) -> Result(String, String) {
149 use checked <- result.try(load(source))
150 Ok(value.to_glint(checked.config))
151}
152
153pub fn dump_json(source: String) -> Result(String, String) {
154 use checked <- result.try(load(source))
155 Ok(value.to_json(checked.config))
156}
157
158// ── internals ───────────────────────────────────────────────────────
159
160fn path_parts(path: String) -> List(String) {
161 string.split(path, on: ".")
162 |> list.filter(fn(p) { p != "" })
163}
164
165fn dig(value: Value, parts: List(String)) -> Value {
166 case parts {
167 [] -> value
168 [key, ..rest] ->
169 case value.get(value, key) {
170 Ok(next) -> dig(next, rest)
171 Error(e) -> panic as e
172 }
173 }
174}
175
176fn expect(path: String, r: Result(a, String)) -> a {
177 case r {
178 Ok(v) -> v
179 Error(e) -> panic as { path <> ": " <> e }
180 }
181}