use apimock_routing::{
Respond, RuleSet,
rule_set::rule::{
Rule,
when::{
When,
condition_statement::ConditionStatement,
request::{
Request, body::{body_kind::BodyKind, BodyConditionStatement},
http_method::HttpMethod,
url_path::UrlPathConfig,
},
},
},
};
use toml::{Value, value::Table};
use crate::{Config, ListenerConfig, ServiceConfig, config::log_config::LogConfig};
pub fn render_apimock_toml(config: &Config) -> String {
let mut root = Table::new();
if let Some(listener) = config.listener.as_ref() {
root.insert("listener".to_owned(), Value::Table(listener_table(listener)));
}
if let Some(log) = config.log.as_ref() {
if let Some(t) = log_table(log) {
root.insert("log".to_owned(), Value::Table(t));
}
}
root.insert("service".to_owned(), Value::Table(service_table(&config.service)));
if let Some(ftv) = config.file_tree_view.as_ref() {
if let Some(t) = file_tree_view_table(ftv) {
root.insert("file_tree_view".to_owned(), Value::Table(t));
}
}
toml::to_string_pretty(&Value::Table(root))
.unwrap_or_else(|err| format!("# failed to render apimock.toml: {}\n", err))
}
pub fn render_rule_set_toml(rule_set: &RuleSet) -> String {
let mut root = Table::new();
if let Some(prefix) = rule_set.prefix.as_ref() {
let mut p = Table::new();
if let Some(url) = prefix.url_path_prefix.as_ref() {
p.insert("url_path".to_owned(), Value::String(url.clone()));
}
if let Some(dir) = prefix.respond_dir_prefix.as_ref() {
p.insert("respond_dir".to_owned(), Value::String(dir.clone()));
}
if !p.is_empty() {
root.insert("prefix".to_owned(), Value::Table(p));
}
}
if !rule_set.rules.is_empty() {
let rules: Vec<Value> = rule_set
.rules
.iter()
.map(|r| Value::Table(rule_table(r)))
.collect();
root.insert("rules".to_owned(), Value::Array(rules));
}
toml::to_string_pretty(&Value::Table(root))
.unwrap_or_else(|err| format!("# failed to render rule set: {}\n", err))
}
fn listener_table(l: &ListenerConfig) -> Table {
let mut t = Table::new();
t.insert("ip_address".to_owned(), Value::String(l.ip_address.clone()));
t.insert(
"port".to_owned(),
Value::Integer(i64::from(l.port)),
);
if let Some(tls) = l.tls.as_ref() {
let mut tt = Table::new();
tt.insert("cert".to_owned(), Value::String(tls.cert.clone()));
tt.insert("key".to_owned(), Value::String(tls.key.clone()));
if let Some(p) = tls.port {
tt.insert("port".to_owned(), Value::Integer(i64::from(p)));
}
t.insert("tls".to_owned(), Value::Table(tt));
}
t
}
fn log_table(l: &LogConfig) -> Option<Table> {
let mut t = Table::new();
let v = &l.verbose;
let mut verbose = Table::new();
verbose.insert("header".to_owned(), Value::Boolean(v.header));
verbose.insert("body".to_owned(), Value::Boolean(v.body));
t.insert("verbose".to_owned(), Value::Table(verbose));
Some(t)
}
fn file_tree_view_table(
c: &crate::config::file_tree_config::FileTreeViewConfig,
) -> Option<Table> {
let is_default = !c.show_hidden
&& c.builtin_excludes
&& c.extra_excludes.is_empty()
&& c.include.is_empty();
if is_default {
return None;
}
let mut t = Table::new();
if c.show_hidden {
t.insert("show_hidden".to_owned(), Value::Boolean(true));
}
if !c.builtin_excludes {
t.insert("builtin_excludes".to_owned(), Value::Boolean(false));
}
if !c.extra_excludes.is_empty() {
let arr = toml::value::Array::from_iter(
c.extra_excludes.iter().map(|s| Value::String(s.clone())),
);
t.insert("extra_excludes".to_owned(), Value::Array(arr));
}
if !c.include.is_empty() {
let arr = toml::value::Array::from_iter(
c.include.iter().map(|s| Value::String(s.clone())),
);
t.insert("include".to_owned(), Value::Array(arr));
}
Some(t)
}
fn service_table(s: &ServiceConfig) -> Table {
let mut t = Table::new();
if let Some(strategy) = s.strategy.as_ref() {
t.insert(
"strategy".to_owned(),
Value::String(format!("{}", strategy)),
);
}
if let Some(paths) = s.rule_sets_file_paths.as_ref() {
let arr: Vec<Value> = paths.iter().map(|p| Value::String(p.clone())).collect();
t.insert("rule_sets".to_owned(), Value::Array(arr));
}
if let Some(paths) = s.middlewares_file_paths.as_ref() {
let arr: Vec<Value> = paths.iter().map(|p| Value::String(p.clone())).collect();
t.insert("middlewares".to_owned(), Value::Array(arr));
}
t.insert(
"fallback_respond_dir".to_owned(),
Value::String(s.fallback_respond_dir.clone()),
);
t
}
pub(crate) fn rule_table(r: &Rule) -> Table {
let mut t = Table::new();
t.insert("when".to_owned(), Value::Table(when_table(&r.when)));
t.insert("respond".to_owned(), Value::Table(respond_table(&r.respond)));
t
}
fn when_table(w: &When) -> Table {
let mut t = Table::new();
t.insert("request".to_owned(), Value::Table(request_table(&w.request)));
t
}
fn request_table(req: &Request) -> Table {
let mut t = Table::new();
if let Some(url_path_config) = req.url_path_config.as_ref() {
match url_path_config {
UrlPathConfig::Simple(s) => {
t.insert("url_path".to_owned(), Value::String(s.clone()));
}
UrlPathConfig::Detailed(detail) => {
let mut dt = Table::new();
dt.insert("value".to_owned(), Value::String(detail.value.clone()));
if let Some(op) = detail.op.as_ref() {
dt.insert(
"op".to_owned(),
Value::String(apimock_routing::view::build::op_name(op)),
);
}
t.insert("url_path".to_owned(), Value::Table(dt));
}
}
}
if let Some(method) = req.http_method.as_ref() {
t.insert("method".to_owned(), Value::String(http_method_name(method)));
}
if let Some(headers) = req.headers.as_ref() {
let mut headers_table = Table::new();
let mut keys: Vec<&String> = headers.0.keys().collect();
keys.sort();
for key in keys {
let stmt = &headers.0[key];
headers_table.insert(key.clone(), Value::Table(condition_statement_table(stmt)));
}
if !headers_table.is_empty() {
t.insert("headers".to_owned(), Value::Table(headers_table));
}
}
if let Some(body) = req.body.as_ref() {
let mut body_table = Table::new();
let mut kinds: Vec<&BodyKind> = body.0.keys().collect();
kinds.sort_by_key(|k| body_kind_key(k));
for kind in kinds {
let kind_str = body_kind_key(kind);
let inner = &body.0[kind];
let mut kind_table = Table::new();
let mut keys: Vec<&String> = inner.keys().collect();
keys.sort();
for key in keys {
let stmt = &inner[key];
kind_table.insert(key.clone(), Value::Table(body_condition_statement_table(stmt)));
}
if !kind_table.is_empty() {
body_table.insert(kind_str.to_owned(), Value::Table(kind_table));
}
}
if !body_table.is_empty() {
t.insert("body".to_owned(), Value::Table(body_table));
}
}
t
}
fn condition_statement_table(stmt: &ConditionStatement) -> Table {
let mut t = Table::new();
if let Some(op) = stmt.op.as_ref() {
t.insert(
"op".to_owned(),
Value::String(apimock_routing::view::build::op_name(op)),
);
}
t.insert("value".to_owned(), Value::String(stmt.value.clone()));
t
}
fn body_condition_statement_table(stmt: &BodyConditionStatement) -> Table {
use apimock_routing::view::build::body_op_name_pub;
let mut t = Table::new();
if let Some(op) = stmt.op.as_ref() {
t.insert("op".to_owned(), Value::String(body_op_name_pub(op)));
}
t.insert("value".to_owned(), Value::String(stmt.value.clone()));
t
}
fn body_kind_key(kind: &BodyKind) -> &'static str {
match kind {
BodyKind::Json => "json",
}
}
fn http_method_name(m: &HttpMethod) -> String {
m.as_str().to_owned()
}
fn respond_table(r: &Respond) -> Table {
let mut t = Table::new();
if let Some(p) = r.file_path.as_ref() {
t.insert("file_path".to_owned(), Value::String(p.clone()));
}
if let Some(k) = r.csv_records_key.as_ref() {
t.insert("csv_records_key".to_owned(), Value::String(k.clone()));
}
if let Some(text) = r.text.as_ref() {
t.insert("text".to_owned(), Value::String(text.clone()));
}
if let Some(s) = r.status.as_ref() {
t.insert("status".to_owned(), Value::Integer(i64::from(*s)));
}
if let Some(headers) = r.headers.as_ref() {
let mut ht = Table::new();
for (k, v) in headers.iter() {
match v {
Some(val) => ht.insert(k.clone(), Value::String(val.clone())),
None => ht.insert(k.clone(), Value::Boolean(false)),
};
}
t.insert("headers".to_owned(), Value::Table(ht));
}
if let Some(d) = r.delay_response_milliseconds.as_ref() {
t.insert(
"delay_response_milliseconds".to_owned(),
Value::Integer(i64::from(*d)),
);
}
t
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_rule_set(toml_text: &str) -> RuleSet {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("apimock-rule-set.toml");
std::fs::write(&path, toml_text).expect("write");
RuleSet::new(path.to_str().unwrap(), ".", 0).expect("parse rule set")
}
#[test]
fn round_trip_rule_with_single_header() {
let original = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api\"\n",
"when.request.headers.x-api-key = { value = \"secret\" }\n",
"respond = { text = \"ok\" }\n",
);
let rs = parse_rule_set(original);
assert!(rs.rules[0].when.request.headers.is_some());
let rendered = render_rule_set_toml(&rs);
let rs2 = parse_rule_set(&rendered);
let h = rs2.rules[0]
.when
.request
.headers
.as_ref()
.expect("headers preserved across round trip");
assert!(h.0.contains_key("x-api-key"));
assert_eq!(h.0["x-api-key"].value, "secret");
}
#[test]
fn round_trip_rule_with_header_op() {
let original = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api\"\n",
"when.request.headers.user-agent = { op = \"starts_with\", value = \"Mozilla\" }\n",
"respond = { text = \"ok\" }\n",
);
let rs = parse_rule_set(original);
let rendered = render_rule_set_toml(&rs);
let rs2 = parse_rule_set(&rendered);
let h = rs2.rules[0].when.request.headers.as_ref().unwrap();
let stmt = &h.0["user-agent"];
assert!(matches!(
stmt.op,
Some(apimock_routing::rule_set::rule::when::request::rule_op::RuleOp::StartsWith)
));
assert_eq!(stmt.value, "Mozilla");
}
#[test]
fn round_trip_rule_with_multiple_headers() {
let original = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api\"\n",
"when.request.headers.x-api-key = { value = \"secret\" }\n",
"when.request.headers.x-tenant = { op = \"equal\", value = \"acme\" }\n",
"respond = { text = \"ok\" }\n",
);
let rs = parse_rule_set(original);
let rendered = render_rule_set_toml(&rs);
let rs2 = parse_rule_set(&rendered);
let h = rs2.rules[0].when.request.headers.as_ref().unwrap();
assert_eq!(h.0.len(), 2);
assert!(h.0.contains_key("x-api-key"));
assert!(h.0.contains_key("x-tenant"));
}
#[test]
fn round_trip_rule_with_body_json() {
let original = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api\"\n",
"when.request.body.json.\"user.name\" = { value = \"alice\" }\n",
"respond = { text = \"ok\" }\n",
);
let rs = parse_rule_set(original);
let rendered = render_rule_set_toml(&rs);
let rs2 = parse_rule_set(&rendered);
let b = rs2.rules[0]
.when
.request
.body
.as_ref()
.expect("body preserved across round trip");
let json_kind = apimock_routing::rule_set::rule::when::request::body::body_kind::BodyKind::Json;
let inner = b.0.get(&json_kind).expect("json body kind present");
assert!(inner.contains_key("user.name"));
assert_eq!(inner["user.name"].value, "alice");
}
}