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