1use std::path::PathBuf;
16
17use reddb_wire::{parse as wire_parse, ConnectionTarget, ParseErrorKind};
18
19use crate::error::{ClientError, ErrorCode, Result};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Target {
30 Memory,
32 File { path: PathBuf },
34 Grpc { endpoint: String },
38 GrpcCluster {
43 primary: String,
44 replicas: Vec<String>,
45 force_primary: bool,
46 },
47 Http { base_url: String },
49}
50
51pub fn parse(uri: &str) -> Result<Target> {
53 let target = wire_parse(uri).map_err(|e| match e.kind {
54 ParseErrorKind::Empty => ClientError::new(ErrorCode::InvalidUri, e.message),
55 ParseErrorKind::InvalidUri => ClientError::new(ErrorCode::InvalidUri, e.message),
56 ParseErrorKind::UnsupportedScheme => {
57 let scheme = e
60 .message
61 .strip_prefix("unsupported scheme: ")
62 .unwrap_or(&e.message);
63 ClientError::unsupported_scheme(scheme)
64 }
65 ParseErrorKind::LimitExceeded => {
66 ClientError::new(ErrorCode::InvalidUri, e.message)
70 }
71 })?;
72 Ok(map_target(target))
73}
74
75fn map_target(t: ConnectionTarget) -> Target {
76 match t {
77 ConnectionTarget::Memory => Target::Memory,
78 ConnectionTarget::File { path } => Target::File { path },
79 ConnectionTarget::Grpc { endpoint } => Target::Grpc { endpoint },
80 ConnectionTarget::GrpcCluster {
81 primary,
82 replicas,
83 force_primary,
84 } => Target::GrpcCluster {
85 primary,
86 replicas,
87 force_primary,
88 },
89 ConnectionTarget::Http { base_url } => Target::Http { base_url },
90 ConnectionTarget::RedWire { host, port, tls } => {
94 let _ = tls;
95 Target::Grpc {
96 endpoint: format!("http://{host}:{port}"),
97 }
98 }
99 ConnectionTarget::WsNative { host, port, tls } => {
104 let scheme = if tls { "https" } else { "http" };
105 Target::Http {
106 base_url: format!("{scheme}://{host}:{port}"),
107 }
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn parses_memory() {
118 assert_eq!(parse("memory://").unwrap(), Target::Memory);
119 assert_eq!(parse("memory:").unwrap(), Target::Memory);
120 }
121
122 #[test]
123 fn parses_file_with_absolute_path() {
124 let target = parse("file:///var/lib/reddb/data.rdb").unwrap();
125 match target {
126 Target::File { path } => assert_eq!(path, PathBuf::from("/var/lib/reddb/data.rdb")),
127 _ => panic!("expected File"),
128 }
129 }
130
131 #[test]
132 fn parses_grpc_with_default_port() {
133 let target = parse("grpc://primary.svc.cluster.local").unwrap();
134 match target {
135 Target::Grpc { endpoint } => {
136 assert_eq!(endpoint, "http://primary.svc.cluster.local:55055")
137 }
138 _ => panic!("expected Grpc"),
139 }
140 }
141
142 #[test]
143 fn parses_grpcs_with_default_tls_port() {
144 let target = parse("grpcs://primary.svc.cluster.local").unwrap();
145 match target {
146 Target::Grpc { endpoint } => {
147 assert_eq!(endpoint, "http://primary.svc.cluster.local:55555")
148 }
149 _ => panic!("expected Grpc"),
150 }
151 }
152
153 #[test]
154 fn parses_red_with_default_port() {
155 let target = parse("red://primary.svc.cluster.local").unwrap();
156 match target {
157 Target::Grpc { endpoint } => {
158 assert_eq!(endpoint, "http://primary.svc.cluster.local:5050")
159 }
160 _ => panic!("expected Grpc (back-compat for red://)"),
161 }
162 }
163
164 #[test]
165 fn parses_grpc_with_explicit_port() {
166 let target = parse("grpc://primary:6000").unwrap();
167 match target {
168 Target::Grpc { endpoint } => assert_eq!(endpoint, "http://primary:6000"),
169 _ => panic!("expected Grpc"),
170 }
171 }
172
173 #[test]
174 fn rejects_unknown_scheme() {
175 let err = parse("mongodb://localhost").unwrap_err();
176 assert_eq!(err.code, ErrorCode::UnsupportedScheme);
177 }
178
179 #[test]
180 fn rejects_empty() {
181 assert_eq!(parse("").unwrap_err().code, ErrorCode::InvalidUri);
182 }
183
184 #[test]
185 fn rejects_file_without_path() {
186 assert_eq!(parse("file://").unwrap_err().code, ErrorCode::InvalidUri);
187 }
188
189 #[test]
190 fn parses_grpc_cluster_with_explicit_ports() {
191 let target = parse("grpc://primary:55055,replica1:55055,replica2:55055").unwrap();
192 match target {
193 Target::GrpcCluster {
194 primary,
195 replicas,
196 force_primary,
197 } => {
198 assert_eq!(primary, "http://primary:55055");
199 assert_eq!(
200 replicas,
201 vec!["http://replica1:55055", "http://replica2:55055"]
202 );
203 assert!(!force_primary);
204 }
205 other => panic!("expected GrpcCluster, got {other:?}"),
206 }
207 }
208
209 #[test]
210 fn cluster_inherits_default_port_per_scheme() {
211 match parse("grpc://a,b").unwrap() {
212 Target::GrpcCluster {
213 primary, replicas, ..
214 } => {
215 assert_eq!(primary, "http://a:55055");
216 assert_eq!(replicas, vec!["http://b:55055"]);
217 }
218 other => panic!("expected GrpcCluster, got {other:?}"),
219 }
220 match parse("red://a,b").unwrap() {
221 Target::GrpcCluster {
222 primary, replicas, ..
223 } => {
224 assert_eq!(primary, "http://a:5050");
225 assert_eq!(replicas, vec!["http://b:5050"]);
226 }
227 other => panic!("expected GrpcCluster, got {other:?}"),
228 }
229 }
230
231 #[test]
232 fn cluster_per_host_port_overrides_default() {
233 match parse("grpc://a:7000,b:7001,c").unwrap() {
234 Target::GrpcCluster {
235 primary, replicas, ..
236 } => {
237 assert_eq!(primary, "http://a:7000");
238 assert_eq!(replicas, vec!["http://b:7001", "http://c:55055"]);
239 }
240 other => panic!("expected GrpcCluster, got {other:?}"),
241 }
242 }
243
244 #[test]
245 fn cluster_route_primary_query_param_forces_primary() {
246 match parse("grpc://primary,replica?route=primary").unwrap() {
247 Target::GrpcCluster {
248 primary,
249 replicas,
250 force_primary,
251 } => {
252 assert_eq!(primary, "http://primary:55055");
253 assert_eq!(replicas, vec!["http://replica:55055"]);
254 assert!(force_primary, "?route=primary must set force_primary");
255 }
256 other => panic!("expected GrpcCluster, got {other:?}"),
257 }
258 }
259
260 #[test]
261 fn cluster_rejects_empty_host_entry() {
262 assert_eq!(
263 parse("grpc://primary,,replica").unwrap_err().code,
264 ErrorCode::InvalidUri
265 );
266 assert_eq!(parse("grpc://,b").unwrap_err().code, ErrorCode::InvalidUri);
267 }
268
269 #[test]
270 fn cluster_rejects_invalid_port() {
271 assert_eq!(
272 parse("grpc://a:nope,b:55055").unwrap_err().code,
273 ErrorCode::InvalidUri
274 );
275 }
276
277 #[test]
278 fn single_host_grpc_still_routes_to_grpc_target_not_cluster() {
279 match parse("grpc://primary:55055").unwrap() {
280 Target::Grpc { endpoint } => assert_eq!(endpoint, "http://primary:55055"),
281 other => panic!("expected Grpc (single host), got {other:?}"),
282 }
283 }
284}