spacegate_plugin/
model.rs

1use serde::{Deserialize, Serialize};
2use spacegate_kernel::service::http_route::match_request::HttpPathMatchRewrite;
3
4#[derive(Default, Debug, Serialize, Deserialize, Clone)]
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6pub struct SgHttpPathModifier {
7    /// Type defines the type of path modifier.
8    pub kind: SgHttpPathModifierType,
9    /// Value is the value to be used to replace the path during forwarding.
10    pub value: String,
11}
12
13#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default, Copy)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[serde(rename_all = "PascalCase")]
16pub enum SgHttpPathModifierType {
17    /// This type of modifier indicates that the full path will be replaced by the specified value.
18    ReplaceFullPath,
19    /// This type of modifier indicates that any prefix path matches will be replaced by the substitution value.
20    /// For example, a path with a prefix match of “/foo” and a ReplacePrefixMatch substitution of “/bar” will have the “/foo” prefix replaced with “/bar” in matching requests.
21    #[default]
22    ReplacePrefixMatch,
23    ReplaceRegex,
24}
25
26impl SgHttpPathModifier {
27    pub fn replace(&self, path: &str, path_match: &HttpPathMatchRewrite) -> Option<String> {
28        let value = &self.value;
29        match (self.kind, path_match) {
30            (SgHttpPathModifierType::ReplaceFullPath, _) => {
31                if value.eq_ignore_ascii_case(path) {
32                    Some(value.clone())
33                } else {
34                    None
35                }
36            }
37            (SgHttpPathModifierType::ReplacePrefixMatch, HttpPathMatchRewrite::Prefix(prefix, _)) => {
38                fn not_empty(s: &&str) -> bool {
39                    !s.is_empty()
40                }
41                let mut path_segments = path.split('/').filter(not_empty);
42                let mut prefix_segments = prefix.split('/').filter(not_empty);
43                loop {
44                    match (path_segments.next(), prefix_segments.next()) {
45                        (Some(path_seg), Some(prefix_seg)) => {
46                            if !path_seg.eq_ignore_ascii_case(prefix_seg) {
47                                return None;
48                            }
49                        }
50                        (None, None) => {
51                            // handle with duplicated stash and no stash
52                            let mut new_path = String::from("/");
53                            new_path.push_str(self.value.trim_start_matches('/'));
54                            return Some(new_path);
55                        }
56                        (Some(rest_path), None) => {
57                            let mut new_path = String::from("/");
58                            let replace_value = self.value.trim_matches('/');
59                            new_path.push_str(replace_value);
60                            if !replace_value.is_empty() {
61                                new_path.push('/');
62                            }
63                            new_path.push_str(rest_path);
64                            for seg in path_segments {
65                                new_path.push('/');
66                                new_path.push_str(seg);
67                            }
68                            if path.ends_with('/') {
69                                new_path.push('/')
70                            }
71                            return Some(new_path);
72                        }
73                        (None, Some(_)) => return None,
74                    }
75                }
76            }
77            (SgHttpPathModifierType::ReplaceRegex, HttpPathMatchRewrite::RegExp(re, _)) => Some(re.replace(path, value).to_string()),
78            _ => None,
79        }
80    }
81}
82
83#[test]
84fn test_prefix_replace() {
85    let modifier = SgHttpPathModifier {
86        kind: SgHttpPathModifierType::ReplacePrefixMatch,
87        value: "/iam".into(),
88    };
89    let replace = HttpPathMatchRewrite::prefix("api/iam");
90    assert_eq!(Some("/iam/get_name"), modifier.replace("api/iam/get_name", &replace).as_deref());
91    assert_eq!(Some("/iam/get_name/example.js"), modifier.replace("api/iam/get_name/example.js", &replace).as_deref());
92    assert_eq!(Some("/iam/get_name/"), modifier.replace("api/iam/get_name/", &replace).as_deref());
93}
94
95#[test]
96fn test_regex_replace() {
97    let modifier = SgHttpPathModifier {
98        kind: SgHttpPathModifierType::ReplaceRegex,
99        value: "/path/$1/subpath$2".into(),
100    };
101    let replace = HttpPathMatchRewrite::regex(regex::Regex::new(r"/api/(\w*)/subpath($|/.*)").expect("invalid regex"));
102    assert_eq!(Some("/path/iam/subpath/get_name"), modifier.replace("/api/iam/subpath/get_name", &replace).as_deref());
103    assert_eq!(Some("/path/iam/subpath/"), modifier.replace("/api/iam/subpath/", &replace).as_deref());
104    assert_eq!(Some("/path/iam/subpath"), modifier.replace("/api/iam/subpath", &replace).as_deref());
105    // won't match
106    assert_eq!(Some("/api/iam/subpath2"), modifier.replace("/api/iam/subpath2", &replace).as_deref());
107}