1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
9pub struct Settings {
10 pub ttl: Option<u64>,
12 pub cached_endpoints: Vec<CachedEndpoint>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct CachedEndpoint {
19 pub method: Method,
21 pub path: Path,
23}
24
25impl CachedEndpoint {
26 pub fn new(method: Method, path: Path) -> Self {
28 Self { method, path }
29 }
30}
31
32impl Path {
33 pub fn custom_mint(method: &str) -> Self {
35 Path::Custom(format!("/v1/mint/{}", encode_path_segment(method)))
36 }
37
38 pub fn custom_melt(method: &str) -> Self {
40 Path::Custom(format!("/v1/melt/{}", encode_path_segment(method)))
41 }
42}
43
44fn encode_path_segment(segment: &str) -> String {
45 let mut encoded = String::with_capacity(segment.len());
46 for byte in segment.bytes() {
47 match byte {
48 b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' => {
49 encoded.push(byte as char);
50 }
51 _ => {
52 encoded.push_str(&format!("%{byte:02X}"));
53 }
54 }
55 }
56 encoded
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "UPPERCASE")]
62pub enum Method {
63 Get,
65 Post,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub enum Path {
72 Swap,
74 Custom(String),
76}
77
78impl Serialize for Path {
79 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80 where
81 S: serde::Serializer,
82 {
83 let s = match self {
84 Path::Swap => "/v1/swap",
85 Path::Custom(custom) => custom.as_str(),
86 };
87 serializer.serialize_str(s)
88 }
89}
90
91impl<'de> Deserialize<'de> for Path {
92 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93 where
94 D: serde::Deserializer<'de>,
95 {
96 let s = String::deserialize(deserializer)?;
97 Ok(match s.as_str() {
98 "/v1/swap" => Path::Swap,
99 custom => Path::Custom(custom.to_string()),
100 })
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn custom_mint_method_cannot_inject_path_segments() {
110 for method in ["../../v1/swap", "..", "."] {
111 let s = match Path::custom_mint(method) {
112 Path::Custom(s) => s,
113 other => panic!("expected Path::Custom, got {other:?}"),
114 };
115 let segments: Vec<&str> = s.trim_start_matches('/').split('/').collect();
116
117 assert_eq!(
118 segments.len(),
119 3,
120 "method injected extra path segments: {s}"
121 );
122 assert!(
123 !segments.iter().any(|seg| *seg == ".." || *seg == "."),
124 "method injected a path-traversal segment: {s}"
125 );
126 }
127 }
128
129 #[test]
130 fn custom_melt_method_cannot_inject_path_segments() {
131 for method in ["../../v1/swap", "..", "."] {
132 let s = match Path::custom_melt(method) {
133 Path::Custom(s) => s,
134 other => panic!("expected Path::Custom, got {other:?}"),
135 };
136 let segments: Vec<&str> = s.trim_start_matches('/').split('/').collect();
137
138 assert_eq!(
139 segments.len(),
140 3,
141 "method injected extra path segments: {s}"
142 );
143 assert!(
144 !segments.iter().any(|seg| *seg == ".." || *seg == "."),
145 "method injected a path-traversal segment: {s}"
146 );
147 }
148 }
149}