1use crate::{Error, Request};
2
3pub trait Substituter {
5 type Context;
7
8 fn visit_identity(
10 &self,
11 value: &str,
12 context: &Request<Self::Context>,
13 ) -> Result<String, Error>;
14
15 fn visit_operation(
17 &self,
18 value: &str,
19 context: &Request<Self::Context>,
20 ) -> Result<String, Error>;
21
22 fn visit_resource(
24 &self,
25 value: &str,
26 context: &Request<Self::Context>,
27 ) -> Result<String, Error>;
28}
29
30pub(crate) const ANY_VAR: &str = "{{any}}";
31pub(crate) const IDENTITY_VAR: &str = "{{identity}}";
32pub(crate) const OPERATION_VAR: &str = "{{operation}}";
33
34#[derive(Debug)]
39pub struct DefaultSubstituter;
40
41impl Substituter for DefaultSubstituter {
42 type Context = ();
43
44 fn visit_identity(
45 &self,
46 value: &str,
47 context: &Request<Self::Context>,
48 ) -> Result<String, Error> {
49 Ok(replace_identity(value, context))
50 }
51
52 fn visit_operation(
53 &self,
54 value: &str,
55 context: &Request<Self::Context>,
56 ) -> Result<String, Error> {
57 Ok(replace_operation(value, context))
58 }
59
60 fn visit_resource(
61 &self,
62 value: &str,
63 context: &Request<Self::Context>,
64 ) -> Result<String, Error> {
65 Ok(replace_resource(value, context))
66 }
67}
68
69fn replace_identity<RC>(value: &str, context: &Request<RC>) -> String {
70 let mut result = value.to_owned();
71 for variable in VariableIter::new(value) {
72 result = match variable {
73 ANY_VAR | IDENTITY_VAR => replace(&result, variable, context.identity()),
74 _ => result,
75 };
76 }
77 result
78}
79
80fn replace_operation<RC>(value: &str, context: &Request<RC>) -> String {
81 let mut result = value.to_owned();
82 for variable in VariableIter::new(value) {
83 result = match variable {
84 ANY_VAR | OPERATION_VAR => replace(&result, variable, context.operation()),
85 IDENTITY_VAR => replace(&result, variable, context.identity()),
86 _ => result,
87 };
88 }
89 result
90}
91
92fn replace_resource<RC>(value: &str, context: &Request<RC>) -> String {
93 let mut result = value.to_owned();
94 for variable in VariableIter::new(value) {
95 result = match variable {
96 ANY_VAR => replace(&result, variable, context.resource()),
97 IDENTITY_VAR => replace(&result, variable, context.identity()),
98 OPERATION_VAR => replace(&result, variable, context.operation()),
99 _ => result,
100 };
101 }
102 result
103}
104
105fn replace(value: &str, variable: &str, substitution: &str) -> String {
106 value.replace(variable, substitution)
107}
108
109#[derive(Debug)]
114pub struct VariableIter<'a> {
115 value: &'a str,
116 index: usize,
117}
118
119impl<'a> VariableIter<'a> {
120 pub fn new(value: &'a str) -> Self {
121 Self { value, index: 0 }
122 }
123}
124
125impl<'a> Iterator for VariableIter<'a> {
126 type Item = &'a str;
127
128 fn next(&mut self) -> Option<Self::Item> {
129 let value = &self.value[self.index..];
130 if let Some(start) = value.find("{{") {
131 if let Some(end) = value.find("}}") {
132 if start < end {
133 self.index = self.index + end + 2;
134 return Some(&value[start..end + 2]);
135 }
136 }
137 }
138 None
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use proptest::prelude::*;
145 use test_case::test_case;
146
147 use super::*;
148
149 #[test_case("{{any}}",
150 "some_identity",
151 "some_operation",
152 "some_resource",
153 "some_identity";
154 "any var substitution")]
155 #[test_case("{{identity}}",
156 "some_identity",
157 "some_operation",
158 "some_resource",
159 "some_identity";
160 "identity var substitution")]
161 fn visit_identity_test(
162 input: &str,
163 identity: &str,
164 operation: &str,
165 resource: &str,
166 expected: &str,
167 ) {
168 let request = Request::new(identity, operation, resource).unwrap();
169
170 assert_eq!(
171 expected,
172 DefaultSubstituter.visit_identity(input, &request).unwrap()
173 );
174 }
175
176 #[test_case("{{any}}",
177 "some_identity",
178 "some_operation",
179 "some_resource",
180 "some_operation";
181 "any var substitution")]
182 #[test_case("{{operation}}",
183 "some_identity",
184 "some_operation",
185 "some_resource",
186 "some_operation";
187 "operation var substitution")]
188 #[test_case("{{identity}}",
189 "some_identity",
190 "some_operation",
191 "some_resource",
192 "some_identity";
193 "identity var substitution")]
194 #[test_case("prefix-{{identity}}-suffix",
195 "some_identity",
196 "some_operation",
197 "some_resource",
198 "prefix-some_identity-suffix";
199 "contains identity var substitution")]
200 #[test_case("prefix-{{identity}}-contains-{{identity}}-suffix",
201 "some_identity",
202 "some_operation",
203 "some_resource",
204 "prefix-some_identity-contains-some_identity-suffix";
205 "multiple vars substitution")]
206 fn visit_operation_test(
207 input: &str,
208 identity: &str,
209 operation: &str,
210 resource: &str,
211 expected: &str,
212 ) {
213 let request = Request::new(identity, operation, resource).unwrap();
214
215 assert_eq!(
216 expected,
217 DefaultSubstituter.visit_operation(input, &request).unwrap()
218 );
219 }
220
221 #[test_case("{{any}}",
222 "some_identity",
223 "some_operation",
224 "some_resource",
225 "some_resource";
226 "any var substitution")]
227 #[test_case("{{operation}}",
228 "some_identity",
229 "some_operation",
230 "some_resource",
231 "some_operation";
232 "operation var substitution")]
233 #[test_case("{{identity}}",
234 "some_identity",
235 "some_operation",
236 "some_resource",
237 "some_identity";
238 "identity var substitution")]
239 #[test_case("home/{{identity}}/middle/{{operation}}/last",
240 "some_identity",
241 "some_operation",
242 "some_resource",
243 "home/some_identity/middle/some_operation/last";
244 "contains multiple vars substitution")]
245 fn visit_resource_test(
246 input: &str,
247 identity: &str,
248 operation: &str,
249 resource: &str,
250 expected: &str,
251 ) {
252 let request = Request::new(identity, operation, resource).unwrap();
253
254 assert_eq!(
255 expected,
256 DefaultSubstituter.visit_resource(input, &request).unwrap()
257 );
258 }
259
260 proptest! {
261 #[test]
262 fn iterator_does_not_crash(value in "[a-z\\{\\}]+") {
263 let _ = VariableIter::new(&value).collect::<Vec<_>>();
264 }
265 }
266}