1mod port_forward;
10mod redirect_https;
11mod reverse_proxy;
12mod static_site;
13
14use serde_json::Value;
15
16use crate::error::Error;
17use crate::rule::{ListenSpec, RawRule, SourceInfo, TlsConfig};
18
19#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
23pub struct PresetInvocation {
24 pub name: String,
27 pub preset: String,
30 #[serde(deserialize_with = "crate::rule::de_listen_non_empty")]
31 pub listen: Vec<ListenSpec>,
32 #[serde(default)]
33 pub args: Value,
34 #[serde(default)]
40 pub tls: Option<TlsConfig>,
41 #[serde(default)]
42 pub source: SourceInfo,
43}
44
45#[derive(Debug, Clone, serde::Serialize)]
52#[serde(untagged)]
53pub enum RuleEntry {
54 Preset(PresetInvocation),
55 Raw(RawRule),
56}
57
58impl<'de> serde::Deserialize<'de> for RuleEntry {
59 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
60 let v = Value::deserialize(d)?;
61 if v.get("preset").is_some() {
62 let inv: PresetInvocation = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
63 Ok(Self::Preset(inv))
64 } else {
65 let r: RawRule = serde_json::from_value(v).map_err(serde::de::Error::custom)?;
66 Ok(Self::Raw(r))
67 }
68 }
69}
70
71pub fn expand_invocation(inv: PresetInvocation) -> Result<Vec<RawRule>, Error> {
77 match inv.preset.as_str() {
78 "reverse_proxy" => reverse_proxy::expand(inv),
79 "port_forward" => port_forward::expand(inv),
80 "static_site" => static_site::expand(inv),
81 "redirect_https" => redirect_https::expand(inv),
82 other => Err(Error::compile(format!(
83 "unknown preset {other:?}; supported: reverse_proxy / port_forward / static_site / redirect_https"
84 ))),
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn unknown_preset_name_yields_compile_error() {
94 let inv = PresetInvocation {
95 name: "x".into(),
96 preset: "no_such_preset".into(),
97 listen: vec![":443".into()],
98 args: Value::Null,
99 tls: None,
100 source: SourceInfo::default(),
101 };
102 let err = expand_invocation(inv).expect_err("unknown preset must fail");
103 let msg = err.to_string();
104 assert!(msg.contains("no_such_preset"), "error names the offending preset: {msg}");
105 assert!(msg.contains("reverse_proxy"), "error lists supported presets: {msg}");
106 }
107
108 #[test]
109 fn rule_entry_deserializes_preset_when_preset_key_present() {
110 let raw = serde_json::json!({
111 "preset": "port_forward",
112 "name": "ssh",
113 "listen": [":2222"],
114 "args": { "upstream": "10.0.0.5:22" }
115 });
116 let entry: RuleEntry = serde_json::from_value(raw).expect("parse preset entry");
117 match entry {
118 RuleEntry::Preset(inv) => {
119 assert_eq!(inv.preset, "port_forward");
120 assert_eq!(inv.name, "ssh");
121 assert_eq!(inv.listen, vec![":2222".to_string()]);
122 }
123 RuleEntry::Raw(_) => panic!("preset key must route to Preset variant"),
124 }
125 }
126
127 #[test]
128 fn rule_entry_deserializes_raw_when_no_preset_key() {
129 let raw = serde_json::json!({
130 "name": "r",
131 "listen": [":443"],
132 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" }
133 });
134 let entry: RuleEntry = serde_json::from_value(raw).expect("parse raw entry");
135 match entry {
136 RuleEntry::Raw(r) => assert_eq!(r.name, "r"),
137 RuleEntry::Preset(_) => panic!("no preset key must route to Raw variant"),
138 }
139 }
140}