Skip to main content

apimock_config/config/
service_config.rs

1//! The `[service]` section of `apimock.toml`.
2//!
3//! # What was here before 5.0
4//!
5//! Pre-5.0, `ServiceConfig` also held `Vec<MiddlewareHandler>` and had
6//! `middleware_response` / `rule_set_response` methods that built
7//! `hyper::Response` values. Those methods violated the 5.0 crate
8//! boundary (config owns declarative data, not HTTP responses), so
9//! they moved:
10//!
11//! - Compiled Rhai middlewares live in `apimock-server` now, inside a
12//!   new `LoadedMiddlewares` type built from this struct's
13//!   `middlewares_file_paths` at startup.
14//! - HTTP dispatch methods live on the server's request-handling path.
15//!
16//! What stays here is the editable, serde-deserialisable data that a
17//! GUI would show in its `[service]` panel.
18
19use apimock_routing::{RuleSet, Strategy};
20use console::style;
21use serde::Deserialize;
22use util::canonicalized_fallback_respond_dir_to_print;
23
24use std::path::Path;
25
26mod util;
27
28use super::constant::{PRINT_DELIMITER, SERVICE_DEFAULT_FALLBACK_RESPOND_DIR};
29
30#[derive(Clone, Deserialize)]
31pub struct ServiceConfig {
32    /// How multiple matching rules in a set are resolved. Currently
33    /// `first_match` is the only recognised value.
34    pub strategy: Option<Strategy>,
35
36    /// Paths to rule-set TOML files. User-editable via the `[service]`
37    /// table.
38    #[serde(rename = "rule_sets")]
39    pub rule_sets_file_paths: Option<Vec<String>>,
40
41    /// Loaded rule sets in the same order as `rule_sets_file_paths`.
42    /// Populated at startup by `Config::new` (this field is
43    /// `#[serde(skip)]` because it comes from loading the files listed
44    /// above, not from the config TOML itself).
45    #[serde(skip)]
46    pub rule_sets: Vec<RuleSet>,
47
48    /// Paths to Rhai middleware files. Compilation happens in the
49    /// server crate — config only holds the paths.
50    #[serde(rename = "middlewares")]
51    pub middlewares_file_paths: Option<Vec<String>>,
52
53    /// Filesystem directory served by the dyn-route fallback.
54    pub fallback_respond_dir: String,
55}
56
57impl ServiceConfig {
58    /// Validate that every rule set and the fallback respond dir are
59    /// internally consistent.
60    ///
61    /// # Why validation stays in config, not routing
62    ///
63    /// Validation inspects cross-cutting state (file existence of the
64    /// fallback dir, every rule's respond.file_path, etc.). All that
65    /// state is assembled by config loading. Routing has per-rule
66    /// validators, which this method calls.
67    pub fn validate(&self) -> bool {
68        let rule_sets_validate = self.rule_sets
69            .iter()
70            .enumerate()
71            .all(|(rule_set_idx, rule_set)| {
72                let prefix_validate = rule_set.prefix.is_none()
73                    || rule_set.prefix.as_ref().unwrap().validate(rule_set_idx);
74
75                let default_validate =
76                    rule_set.default.is_none() || rule_set.default.as_ref().unwrap().validate();
77
78                let guard_validate =
79                    rule_set.guard.is_none() || rule_set.guard.as_ref().unwrap().validate();
80
81                let dir_prefix = rule_set.dir_prefix();
82                let rules_validate = rule_set.rules.iter().enumerate().all(|(rule_idx, rule)| {
83                    rule.when.validate(rule_idx, rule_set_idx)
84                        && rule
85                            .respond
86                            .validate(dir_prefix.as_str(), rule_idx, rule_set_idx)
87                });
88
89                prefix_validate && default_validate && guard_validate && rules_validate
90            });
91        if !rule_sets_validate {
92            log::error!("something wrong in rule sets");
93        }
94
95        let fallback_respond_dir_validate = Path::new(self.fallback_respond_dir.as_str()).exists();
96        if !fallback_respond_dir_validate {
97            log::error!(
98                "{} fallback_respond_dir: {}",
99                style("invalid").red(),
100                self.fallback_respond_dir
101            );
102        }
103
104        rule_sets_validate && fallback_respond_dir_validate
105    }
106}
107
108impl Default for ServiceConfig {
109    fn default() -> Self {
110        ServiceConfig {
111            strategy: Some(Strategy::default()),
112            rule_sets_file_paths: None,
113            rule_sets: vec![],
114            middlewares_file_paths: None,
115            fallback_respond_dir: SERVICE_DEFAULT_FALLBACK_RESPOND_DIR.to_owned(),
116        }
117    }
118}
119
120impl std::fmt::Display for ServiceConfig {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        let has_rule_sets = !self.rule_sets.is_empty();
123
124        if has_rule_sets {
125            let _ = writeln!(
126                f,
127                "[rule_sets.strategy] {}",
128                self.strategy.clone().unwrap_or_default()
129            );
130            let _ = writeln!(f, "");
131        }
132
133        for (idx, rule_set) in self.rule_sets.iter().enumerate() {
134            let _ = writeln!(
135                f,
136                "@ rule_set #{} ({})\n",
137                idx + 1,
138                style(rule_set.file_path.as_str()).green()
139            );
140            let _ = write!(f, "{}\n", rule_set);
141        }
142
143        if has_rule_sets {
144            let _ = writeln!(f, "{}", PRINT_DELIMITER);
145        }
146
147        let _ = writeln!(
148            f,
149            "[fallback_respond_dir] {}",
150            canonicalized_fallback_respond_dir_to_print(self.fallback_respond_dir.as_str())
151        );
152
153        Ok(())
154    }
155}