fleetflow_atom/
parser.rs

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
8/// KDLファイルをパースしてFlowを生成
9pub 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
21/// KDL文字列をパース
22pub 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                // TODO: include機能の実装
40            }
41            "variables" => {
42                // TODO: 変数定義の実装
43            }
44            _ => {
45                // 不明なノードは警告してスキップ
46                eprintln!("Warning: Unknown node '{}'", node.name().value());
47            }
48        }
49    }
50
51    Ok(Flow {
52        name,
53        stages,
54        services,
55    })
56}
57
58/// stage ノードをパース
59fn 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                    // service "name" 形式で個別に指定
74                    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
102/// service ノードをパース
103fn 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    // イメージ名の自動推測
186    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
193/// port ノードをパース
194///
195/// サポートされる形式:
196/// - 名前付き引数: port host=8080 container=3000
197/// - 位置引数(後方互換): port 8080 3000
198fn parse_port(node: &KdlNode) -> Option<Port> {
199    // 名前付き引数を優先
200    let host = node
201        .get("host")
202        .and_then(|e| e.value().as_i64())
203        .map(|v| v as u16)
204        .or_else(|| {
205            // フォールバック: 位置引数
206            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            // フォールバック: 位置引数
215            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
241/// volume ノードをパース
242fn 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
260/// サービス名からイメージ名を推測
261fn 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;