1use std::path::Path;
23
24use serde_json;
25
26use crate::rule_set::rule::respond::Respond;
27use crate::rule_set::rule::when::When;
28use crate::rule_set::rule::when::request::Request;
29use crate::rule_set::rule::when::request::http_method::HttpMethod;
30use crate::rule_set::rule::when::request::rule_op::RuleOp;
31use crate::rule_set::rule::when::request::url_path::UrlPathConfig;
32use crate::rule_set::rule::Rule;
33use crate::rule_set::RuleSet;
34
35use crate::view::{
36 BodyConditionView, FileNodeKind, FileNodeView, FileTreeView, HeaderConditionView,
37 RespondView, RouteCatalogSnapshot, RuleSetView, RuleView, ScriptRouteView, UrlPathView,
38 WhenView,
39};
40
41pub fn build_route_catalog(
46 rule_sets: &[RuleSet],
47 fallback_respond_dir: Option<&str>,
48 file_tree: Option<FileTreeView>,
49 script_routes: Vec<ScriptRouteView>,
50) -> RouteCatalogSnapshot {
51 let rule_set_views = rule_sets
52 .iter()
53 .enumerate()
54 .map(|(idx, rs)| build_rule_set_view(rs, idx))
55 .collect();
56
57 RouteCatalogSnapshot {
58 rule_sets: rule_set_views,
59 fallback_respond_dir: fallback_respond_dir.map(str::to_owned),
60 file_tree,
61 script_routes,
62 }
63}
64
65pub fn build_rule_set_view(rule_set: &RuleSet, index: usize) -> RuleSetView {
66 let (url_prefix, dir_prefix) = match rule_set.prefix.as_ref() {
67 Some(p) => (p.url_path_prefix.clone(), p.respond_dir_prefix.clone()),
68 None => (None, None),
69 };
70
71 RuleSetView {
72 index,
73 source_path: rule_set.file_path.clone(),
74 url_path_prefix: url_prefix,
75 respond_dir_prefix: dir_prefix,
76 rules: rule_set
77 .rules
78 .iter()
79 .enumerate()
80 .map(|(idx, r)| build_rule_view(r, idx))
81 .collect(),
82 }
83}
84
85pub fn build_rule_view(rule: &Rule, index: usize) -> RuleView {
86 RuleView {
87 index,
88 when: build_when_view(&rule.when),
89 respond: build_respond_view(&rule.respond),
90 }
91}
92
93pub fn build_when_view(when: &When) -> WhenView {
94 let req: &Request = &when.request;
95 WhenView {
96 url_path: build_url_path_view(req.url_path_config.as_ref()),
97 method: req.http_method.as_ref().map(http_method_name),
98 headers: build_header_condition_views(req.headers.as_ref()),
99 body: build_body_condition_views(req.body.as_ref()),
100 }
101}
102
103fn build_header_condition_views(
104 headers: Option<&crate::rule_set::rule::when::request::headers::Headers>,
105) -> Vec<HeaderConditionView> {
106 let headers = match headers {
107 Some(h) => h,
108 None => return Vec::new(),
109 };
110 headers
112 .0
113 .iter()
114 .map(|(name, stmt)| {
115 let op_str = op_name(stmt.op.as_ref().unwrap_or(&RuleOp::default()));
116 HeaderConditionView {
117 name: name.clone(),
118 op: op_str,
119 value: Some(stmt.value.clone()),
120 }
121 })
122 .collect()
123}
124
125fn build_body_condition_views(
126 body: Option<&crate::rule_set::rule::when::request::body::Body>,
127) -> Vec<BodyConditionView> {
128 use crate::rule_set::rule::when::request::body::body_kind::BodyKind;
129 use crate::rule_set::rule::when::request::body::body_operator::BodyOperator;
130
131 let body = match body {
132 Some(b) => b,
133 None => return Vec::new(),
134 };
135
136 let mut views: Vec<BodyConditionView> = Vec::new();
137 for (kind, conditions) in &body.0 {
138 let kind_str = match kind {
139 BodyKind::Json => "json",
140 };
141 for (path, stmt) in conditions {
142 let op_str = format!(
143 "{}",
144 stmt.op.as_ref().unwrap_or(&BodyOperator::Equal)
145 )
146 .trim()
147 .to_owned();
148 let op_clean = body_op_name(stmt.op.as_ref().unwrap_or(&BodyOperator::Equal));
151 let value = serde_json::from_str::<serde_json::Value>(&stmt.value)
153 .unwrap_or_else(|_| serde_json::Value::String(stmt.value.clone()));
154 let _ = op_str; views.push(BodyConditionView {
156 kind: kind_str.to_owned(),
157 path: path.clone(),
158 op: op_clean,
159 value,
160 });
161 }
162 }
163 views.sort_by(|a, b| a.path.cmp(&b.path));
165 views
166}
167
168pub fn body_op_name_pub(op: &crate::rule_set::rule::when::request::body::body_operator::BodyOperator) -> String {
171 body_op_name(op)
172}
173
174fn body_op_name(op: &crate::rule_set::rule::when::request::body::body_operator::BodyOperator) -> String {
175 use crate::rule_set::rule::when::request::body::body_operator::BodyOperator;
176 match op {
177 BodyOperator::Equal => "equal",
178 BodyOperator::EqualString => "equal_string",
179 BodyOperator::Contains => "contains",
180 BodyOperator::StartsWith => "starts_with",
181 BodyOperator::EndsWith => "ends_with",
182 BodyOperator::Regex => "regex",
183 BodyOperator::EqualTyped => "equal_typed",
184 BodyOperator::EqualNumber => "equal_number",
185 BodyOperator::GreaterThan => "greater_than",
186 BodyOperator::LessThan => "less_than",
187 BodyOperator::GreaterOrEqual => "greater_or_equal",
188 BodyOperator::LessOrEqual => "less_or_equal",
189 BodyOperator::Exists => "exists",
190 BodyOperator::Absent => "absent",
191 BodyOperator::ArrayLengthEqual => "array_length_equal",
192 BodyOperator::ArrayLengthAtLeast => "array_length_at_least",
193 BodyOperator::ArrayContains => "array_contains",
194 BodyOperator::EqualInteger => "equal_integer",
195 }
196 .to_owned()
197}
198
199fn build_url_path_view(cfg: Option<&UrlPathConfig>) -> Option<UrlPathView> {
200 let cfg = cfg?;
201 let (value, op) = match cfg {
202 UrlPathConfig::Simple(s) => (s.clone(), op_name(&RuleOp::default())),
203 UrlPathConfig::Detailed(detail) => {
204 let op = detail
205 .op
206 .as_ref()
207 .map(op_name)
208 .unwrap_or_else(|| op_name(&RuleOp::default()));
209 (detail.value.clone(), op)
210 }
211 };
212 Some(UrlPathView { value, op })
213}
214
215pub fn op_name(op: &RuleOp) -> String {
222 match op {
223 RuleOp::Equal => "equal",
224 RuleOp::NotEqual => "not_equal",
225 RuleOp::StartsWith => "starts_with",
226 RuleOp::Contains => "contains",
227 RuleOp::WildCard => "wild_card",
228 }
229 .to_owned()
230}
231
232fn http_method_name(m: &HttpMethod) -> String {
233 m.as_str().to_owned()
234}
235
236pub fn build_respond_view(respond: &Respond) -> RespondView {
237 if let Some(path) = respond.file_path.as_ref() {
238 return RespondView::File {
239 path: path.clone(),
240 csv_records_key: respond.csv_records_key.clone(),
241 };
242 }
243 if let Some(text) = respond.text.as_ref() {
244 return RespondView::Text {
245 text: text.clone(),
246 status: respond.status,
247 };
248 }
249 if let Some(status) = respond.status {
250 return RespondView::Status { code: status };
251 }
252 RespondView::Text {
256 text: String::new(),
257 status: None,
258 }
259}
260
261pub const BUILTIN_EXCLUDES: &[&str] = &[
270 "target",
271 "node_modules",
272 "dist",
273 "build",
274 "out",
275 "__pycache__",
276 ".venv",
277 "vendor",
278 ".cargo",
279 ".gradle",
280 ".idea",
281 ".vscode",
282];
283
284#[derive(Clone, Debug)]
296pub struct FileTreeFilter {
297 pub show_hidden: bool,
299 pub builtin_excludes: bool,
302 pub extra_excludes: Vec<String>,
305 pub include: Vec<String>,
309}
310
311impl Default for FileTreeFilter {
312 fn default() -> Self {
313 Self {
314 show_hidden: false,
315 builtin_excludes: true,
316 extra_excludes: Vec::new(),
317 include: Vec::new(),
318 }
319 }
320}
321
322impl FileTreeFilter {
323 fn keep(&self, name: &str, is_dir: bool) -> bool {
325 if !self.show_hidden && name.starts_with('.') {
327 return false;
328 }
329 if self.builtin_excludes && BUILTIN_EXCLUDES.contains(&name) {
331 return false;
332 }
333 if self.extra_excludes.iter().any(|e| e == name) {
335 return false;
336 }
337 if !is_dir && !self.include.is_empty() {
339 if !self.include.iter().any(|pat| name.ends_with(pat.as_str())) {
340 return false;
341 }
342 }
343 true
344 }
345}
346
347pub fn build_file_tree(root: &Path) -> Option<FileTreeView> {
353 build_file_tree_with(root, &FileTreeFilter::default())
354}
355
356pub fn build_file_tree_with(root: &Path, filter: &FileTreeFilter) -> Option<FileTreeView> {
362 let entries = std::fs::read_dir(root).ok()?;
363 let mut nodes: Vec<FileNodeView> = Vec::new();
364
365 for entry in entries.flatten() {
366 let path = entry.path();
367 let name = path
368 .file_name()
369 .map(|n| n.to_string_lossy().into_owned())
370 .unwrap_or_default();
371 let metadata = match entry.metadata() {
372 Ok(m) => m,
373 Err(_) => continue,
374 };
375 let is_dir = metadata.is_dir();
376 let kind = if is_dir {
377 FileNodeKind::Directory
378 } else {
379 FileNodeKind::File
380 };
381
382 if !filter.keep(&name, is_dir) {
384 continue;
385 }
386
387 let route_hint = if matches!(kind, FileNodeKind::File) {
388 path.file_stem()
389 .map(|s| format!("/{}", s.to_string_lossy()))
390 } else {
391 None
392 };
393
394 let children = match kind {
395 FileNodeKind::Directory => Some(Vec::new()),
396 FileNodeKind::File => None,
397 };
398
399 nodes.push(FileNodeView {
400 name,
401 path: path.to_string_lossy().into_owned(),
402 kind,
403 route_hint,
404 children,
405 });
406 }
407
408 nodes.sort_by(|a, b| match (&a.kind, &b.kind) {
410 (FileNodeKind::Directory, FileNodeKind::File) => std::cmp::Ordering::Less,
411 (FileNodeKind::File, FileNodeKind::Directory) => std::cmp::Ordering::Greater,
412 _ => a.name.cmp(&b.name),
413 });
414
415 Some(FileTreeView {
416 root_path: root.to_string_lossy().into_owned(),
417 entries: nodes,
418 })
419}
420
421pub fn list_directory(path: &Path) -> Vec<FileNodeView> {
424 list_directory_with(path, &FileTreeFilter::default())
425}
426
427pub fn list_directory_with(path: &Path, filter: &FileTreeFilter) -> Vec<FileNodeView> {
429 build_file_tree_with(path, filter)
430 .map(|t| t.entries)
431 .unwrap_or_default()
432}
433
434pub fn build_script_route_view(index: usize, source_file: &str) -> ScriptRouteView {
441 let display_name = Path::new(source_file)
442 .file_name()
443 .map(|n| n.to_string_lossy().into_owned())
444 .unwrap_or_else(|| source_file.to_owned());
445
446 ScriptRouteView {
447 index,
448 source_file: source_file.to_owned(),
449 display_name,
450 }
451}