1use crate::error::{FlowError, Result};
2use crate::model::*;
3use kdl::{KdlDocument, KdlNode};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8pub fn parse_kdl_file<P: AsRef<Path>>(path: P) -> Result<Flow> {
10 let content = fs::read_to_string(path.as_ref())?;
11 let name = path
12 .as_ref()
13 .parent()
14 .and_then(|p| p.file_name())
15 .and_then(|n| n.to_str())
16 .unwrap_or("unnamed")
17 .to_string();
18 parse_kdl_string(&content, name)
19}
20
21pub fn parse_kdl_string(content: &str, name: String) -> Result<Flow> {
23 let doc: KdlDocument = content.parse()?;
24
25 let mut stages = HashMap::new();
26 let mut services = HashMap::new();
27
28 for node in doc.nodes() {
29 match node.name().value() {
30 "stage" => {
31 let (stage_name, stage) = parse_stage(node)?;
32 stages.insert(stage_name, stage);
33 }
34 "service" => {
35 let (service_name, service) = parse_service(node)?;
36 services.insert(service_name, service);
37 }
38 "include" => {
39 }
41 "variables" => {
42 }
44 _ => {
45 eprintln!("Warning: Unknown node '{}'", node.name().value());
47 }
48 }
49 }
50
51 Ok(Flow {
52 name,
53 stages,
54 services,
55 })
56}
57
58fn parse_stage(node: &KdlNode) -> Result<(String, Stage)> {
60 let name = node
61 .entries()
62 .first()
63 .and_then(|e| e.value().as_string())
64 .ok_or_else(|| FlowError::InvalidConfig("stage requires a name".to_string()))?
65 .to_string();
66
67 let mut stage = Stage::default();
68
69 if let Some(children) = node.children() {
70 for child in children.nodes() {
71 match child.name().value() {
72 "service" => {
73 if let Some(service_name) =
75 child.entries().first().and_then(|e| e.value().as_string())
76 {
77 stage.services.push(service_name.to_string());
78 }
79 }
80 "variables" => {
81 if let Some(vars) = child.children() {
82 for var in vars.nodes() {
83 let key = var.name().value().to_string();
84 let value = var
85 .entries()
86 .first()
87 .and_then(|e| e.value().as_string())
88 .unwrap_or("")
89 .to_string();
90 stage.variables.insert(key, value);
91 }
92 }
93 }
94 _ => {}
95 }
96 }
97 }
98
99 Ok((name, stage))
100}
101
102fn parse_service(node: &KdlNode) -> Result<(String, Service)> {
104 let name = node
105 .entries()
106 .first()
107 .and_then(|e| e.value().as_string())
108 .ok_or_else(|| FlowError::InvalidConfig("service requires a name".to_string()))?
109 .to_string();
110
111 let mut service = Service::default();
112
113 if let Some(children) = node.children() {
114 for child in children.nodes() {
115 match child.name().value() {
116 "image" => {
117 service.image = child
118 .entries()
119 .first()
120 .and_then(|e| e.value().as_string())
121 .map(|s| s.to_string());
122 }
123 "version" => {
124 service.version = child
125 .entries()
126 .first()
127 .and_then(|e| e.value().as_string())
128 .map(|s| s.to_string());
129 }
130 "command" => {
131 service.command = child
132 .entries()
133 .first()
134 .and_then(|e| e.value().as_string())
135 .map(|s| s.to_string());
136 }
137 "ports" => {
138 if let Some(ports) = child.children() {
139 for port_node in ports.nodes() {
140 if port_node.name().value() == "port" {
141 if let Some(port) = parse_port(port_node) {
142 service.ports.push(port);
143 }
144 }
145 }
146 }
147 }
148 "environment" => {
149 if let Some(envs) = child.children() {
150 for env_node in envs.nodes() {
151 let key = env_node.name().value().to_string();
152 let value = env_node
153 .entries()
154 .first()
155 .and_then(|e| e.value().as_string())
156 .unwrap_or("")
157 .to_string();
158 service.environment.insert(key, value);
159 }
160 }
161 }
162 "volumes" => {
163 if let Some(vols) = child.children() {
164 for vol_node in vols.nodes() {
165 if vol_node.name().value() == "volume" {
166 if let Some(volume) = parse_volume(vol_node) {
167 service.volumes.push(volume);
168 }
169 }
170 }
171 }
172 }
173 "depends_on" => {
174 service.depends_on = child
175 .entries()
176 .iter()
177 .filter_map(|e| e.value().as_string().map(|s| s.to_string()))
178 .collect();
179 }
180 _ => {}
181 }
182 }
183 }
184
185 if service.image.is_none() {
187 service.image = Some(infer_image_name(&name, service.version.as_deref()));
188 }
189
190 Ok((name, service))
191}
192
193fn parse_port(node: &KdlNode) -> Option<Port> {
199 let host = node
201 .get("host")
202 .and_then(|e| e.value().as_i64())
203 .map(|v| v as u16)
204 .or_else(|| {
205 node.entries().get(0)?.value().as_i64().map(|v| v as u16)
207 })?;
208
209 let container = node
210 .get("container")
211 .and_then(|e| e.value().as_i64())
212 .map(|v| v as u16)
213 .or_else(|| {
214 node.entries().get(1)?.value().as_i64().map(|v| v as u16)
216 })?;
217
218 let protocol = node
219 .get("protocol")
220 .and_then(|e| e.value().as_string())
221 .and_then(|s| match s {
222 "tcp" => Some(Protocol::Tcp),
223 "udp" => Some(Protocol::Udp),
224 _ => None,
225 })
226 .unwrap_or_default();
227
228 let host_ip = node
229 .get("host_ip")
230 .and_then(|e| e.value().as_string())
231 .map(|s| s.to_string());
232
233 Some(Port {
234 host,
235 container,
236 protocol,
237 host_ip,
238 })
239}
240
241fn parse_volume(node: &KdlNode) -> Option<Volume> {
243 let entries: Vec<_> = node.entries().iter().collect();
244
245 let host = PathBuf::from(entries.get(0)?.value().as_string()?);
246 let container = PathBuf::from(entries.get(1)?.value().as_string()?);
247
248 let read_only = node
249 .get("read_only")
250 .and_then(|e| e.value().as_bool())
251 .unwrap_or(false);
252
253 Some(Volume {
254 host,
255 container,
256 read_only,
257 })
258}
259
260fn infer_image_name(service_name: &str, version: Option<&str>) -> String {
262 let tag = version.unwrap_or("latest");
263 format!("{}:{}", service_name, tag)
264}
265
266#[cfg(test)]
267mod tests;