1use super::sources::{
2 AtProtoSource, GitSource, HttpSource, JsonFileSource, LocalSource, SlicesSource, Source,
3 SourceType,
4};
5use miette::{Result, miette};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone)]
9pub struct Config {
10 pub output: OutputConfig,
11 pub sources: Vec<Source>,
12}
13
14#[derive(Debug, Clone)]
15pub struct OutputConfig {
16 pub lexicons_dir: PathBuf,
17 pub codegen_dir: PathBuf,
18 pub root_module: Option<String>,
19}
20
21impl Config {
22 pub fn from_kdl(text: &str) -> Result<Self> {
23 let doc = text
24 .parse::<kdl::KdlDocument>()
25 .map_err(|e| miette!("Failed to parse KDL: {}", e))?;
26
27 let mut output: Option<OutputConfig> = None;
28 let mut sources = Vec::new();
29
30 for node in doc.nodes() {
31 match node.name().value() {
32 "output" => {
33 if output.is_some() {
34 return Err(miette!("Multiple output blocks found"));
35 }
36 output = Some(parse_output(node)?);
37 }
38 "source" => {
39 sources.push(parse_source(node)?);
40 }
41 other => {
42 return Err(miette!("Unknown config node: {}", other));
43 }
44 }
45 }
46
47 let output = output.ok_or_else(|| miette!("Missing output block"))?;
48
49 Ok(Config { output, sources })
50 }
51}
52
53fn parse_output(node: &kdl::KdlNode) -> Result<OutputConfig> {
54 let children = node
55 .children()
56 .ok_or_else(|| miette!("output block has no children"))?;
57
58 let mut lexicons_dir: Option<PathBuf> = None;
59 let mut codegen_dir: Option<PathBuf> = None;
60 let mut root_module: Option<String> = None;
61
62 for child in children.nodes() {
63 match child.name().value() {
64 "lexicons" => {
65 let val = child
66 .entries()
67 .get(0)
68 .and_then(|e| e.value().as_string())
69 .ok_or_else(|| miette!("lexicons expects a string value"))?;
70 lexicons_dir = Some(PathBuf::from(val));
71 }
72 "codegen" => {
73 let val = child
74 .entries()
75 .get(0)
76 .and_then(|e| e.value().as_string())
77 .ok_or_else(|| miette!("codegen expects a string value"))?;
78 codegen_dir = Some(PathBuf::from(val));
79 }
80 "root-module" => {
81 let val = child
82 .entries()
83 .get(0)
84 .and_then(|e| e.value().as_string())
85 .ok_or_else(|| miette!("root-module expects a string value"))?;
86 root_module = Some(val.to_string());
87 }
88 other => {
89 return Err(miette!("Unknown output field: {}", other));
90 }
91 }
92 }
93
94 Ok(OutputConfig {
95 lexicons_dir: lexicons_dir.ok_or_else(|| miette!("Missing lexicons directory"))?,
96 codegen_dir: codegen_dir.ok_or_else(|| miette!("Missing codegen directory"))?,
97 root_module,
98 })
99}
100
101fn parse_source(node: &kdl::KdlNode) -> Result<Source> {
102 let name = node
103 .entries()
104 .get(0)
105 .and_then(|e| e.value().as_string())
106 .ok_or_else(|| miette!("source expects a name as first argument"))?
107 .to_string();
108
109 let type_str = node
110 .get("type")
111 .and_then(|v| v.as_string())
112 .ok_or_else(|| miette!("source {} missing type attribute", name))?;
113
114 let priority = node
115 .get("priority")
116 .and_then(|v| v.as_integer())
117 .map(|i| i as i32);
118
119 let children = node
120 .children()
121 .ok_or_else(|| miette!("source {} has no children", name))?;
122
123 let source_type = match type_str {
124 "atproto" => parse_atproto_source(children)?,
125 "git" => parse_git_source(children)?,
126 "http" => parse_http_source(children)?,
127 "jsonfile" => parse_jsonfile_source(children)?,
128 "local" => parse_local_source(children)?,
129 "slices" => parse_slices_source(children)?,
130 other => return Err(miette!("Unknown source type: {}", other)),
131 };
132
133 Ok(Source {
134 name,
135 source_type,
136 explicit_priority: priority,
137 })
138}
139
140fn parse_atproto_source(children: &kdl::KdlDocument) -> Result<SourceType> {
141 let mut endpoint: Option<String> = None;
142 let mut slice: Option<String> = None;
143
144 for child in children.nodes() {
145 match child.name().value() {
146 "endpoint" => {
147 let val = child
148 .entries()
149 .get(0)
150 .and_then(|e| e.value().as_string())
151 .ok_or_else(|| miette!("endpoint expects a string value"))?;
152 endpoint = Some(val.to_string());
153 }
154 "slice" => {
155 let val = child
156 .entries()
157 .get(0)
158 .and_then(|e| e.value().as_string())
159 .ok_or_else(|| miette!("slice expects a string value"))?;
160 slice = Some(val.to_string());
161 }
162 other => {
163 return Err(miette!("Unknown atproto source field: {}", other));
164 }
165 }
166 }
167
168 Ok(SourceType::AtProto(AtProtoSource {
169 endpoint: endpoint.ok_or_else(|| miette!("Missing endpoint"))?,
170 slice,
171 }))
172}
173
174fn parse_git_source(children: &kdl::KdlDocument) -> Result<SourceType> {
175 let mut repo: Option<String> = None;
176 let mut git_ref: Option<String> = None;
177 let mut pattern: Option<String> = None;
178
179 for child in children.nodes() {
180 match child.name().value() {
181 "repo" => {
182 let val = child
183 .entries()
184 .get(0)
185 .and_then(|e| e.value().as_string())
186 .ok_or_else(|| miette!("repo expects a string value"))?;
187 repo = Some(val.to_string());
188 }
189 "ref" => {
190 let val = child
191 .entries()
192 .get(0)
193 .and_then(|e| e.value().as_string())
194 .ok_or_else(|| miette!("ref expects a string value"))?;
195 git_ref = Some(val.to_string());
196 }
197 "pattern" => {
198 let val = child
199 .entries()
200 .get(0)
201 .and_then(|e| e.value().as_string())
202 .ok_or_else(|| miette!("pattern expects a string value"))?;
203 pattern = Some(val.to_string());
204 }
205 other => {
206 return Err(miette!("Unknown git source field: {}", other));
207 }
208 }
209 }
210
211 Ok(SourceType::Git(GitSource {
212 repo: repo.ok_or_else(|| miette!("Missing repo"))?,
213 git_ref,
214 pattern: pattern.unwrap_or_else(|| "**/*.json".to_string()),
215 }))
216}
217
218fn parse_http_source(children: &kdl::KdlDocument) -> Result<SourceType> {
219 let mut url: Option<String> = None;
220
221 for child in children.nodes() {
222 match child.name().value() {
223 "url" => {
224 let val = child
225 .entries()
226 .get(0)
227 .and_then(|e| e.value().as_string())
228 .ok_or_else(|| miette!("url expects a string value"))?;
229 url = Some(val.to_string());
230 }
231 other => {
232 return Err(miette!("Unknown http source field: {}", other));
233 }
234 }
235 }
236
237 Ok(SourceType::Http(HttpSource {
238 url: url.ok_or_else(|| miette!("Missing url"))?,
239 }))
240}
241
242fn parse_jsonfile_source(children: &kdl::KdlDocument) -> Result<SourceType> {
243 let mut path: Option<PathBuf> = None;
244
245 for child in children.nodes() {
246 match child.name().value() {
247 "path" => {
248 let val = child
249 .entries()
250 .get(0)
251 .and_then(|e| e.value().as_string())
252 .ok_or_else(|| miette!("path expects a string value"))?;
253 path = Some(PathBuf::from(val));
254 }
255
256 other => {
257 return Err(miette!("Unknown jsonfile source field: {}", other));
258 }
259 }
260 }
261
262 Ok(SourceType::JsonFile(JsonFileSource {
263 path: path.ok_or_else(|| miette!("Missing path"))?,
264 }))
265}
266
267fn parse_slices_source(children: &kdl::KdlDocument) -> Result<SourceType> {
268 let mut slice: Option<String> = None;
269
270 for child in children.nodes() {
271 match child.name().value() {
272 "slice" => {
273 let val = child
274 .entries()
275 .get(0)
276 .and_then(|e| e.value().as_string())
277 .ok_or_else(|| miette!("slice expects a string value"))?;
278 slice = Some(val.to_string());
279 }
280 other => {
281 return Err(miette!("Unknown slices source field: {}", other));
282 }
283 }
284 }
285
286 Ok(SourceType::Slices(SlicesSource {
287 slice: slice.ok_or_else(|| miette!("Missing slice"))?,
288 }))
289}
290
291fn parse_local_source(children: &kdl::KdlDocument) -> Result<SourceType> {
292 let mut path: Option<PathBuf> = None;
293 let mut pattern: Option<String> = None;
294
295 for child in children.nodes() {
296 match child.name().value() {
297 "path" => {
298 let val = child
299 .entries()
300 .get(0)
301 .and_then(|e| e.value().as_string())
302 .ok_or_else(|| miette!("path expects a string value"))?;
303 path = Some(PathBuf::from(val));
304 }
305 "pattern" => {
306 let val = child
307 .entries()
308 .get(0)
309 .and_then(|e| e.value().as_string())
310 .ok_or_else(|| miette!("pattern expects a string value"))?;
311 pattern = Some(val.to_string());
312 }
313 other => {
314 return Err(miette!("Unknown local source field: {}", other));
315 }
316 }
317 }
318
319 Ok(SourceType::Local(LocalSource {
320 path: path.ok_or_else(|| miette!("Missing path"))?,
321 pattern,
322 }))
323}