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:5055")
137 }
138 _ => panic!("expected Grpc"),
139 }
140 }
141
142 #[test]
143 fn parses_red_with_default_port() {
144 let target = parse("red://primary.svc.cluster.local").unwrap();
145 match target {
146 Target::Grpc { endpoint } => {
147 assert_eq!(endpoint, "http://primary.svc.cluster.local:5050")
148 }
149 _ => panic!("expected Grpc (back-compat for red://)"),
150 }
151 }
152
153 #[test]
154 fn parses_grpc_with_explicit_port() {
155 let target = parse("grpc://primary:6000").unwrap();
156 match target {
157 Target::Grpc { endpoint } => assert_eq!(endpoint, "http://primary:6000"),
158 _ => panic!("expected Grpc"),
159 }
160 }
161
162 #[test]
163 fn rejects_unknown_scheme() {
164 let err = parse("mongodb://localhost").unwrap_err();
165 assert_eq!(err.code, ErrorCode::UnsupportedScheme);
166 }
167
168 #[test]
169 fn rejects_empty() {
170 assert_eq!(parse("").unwrap_err().code, ErrorCode::InvalidUri);
171 }
172
173 #[test]
174 fn rejects_file_without_path() {
175 assert_eq!(parse("file://").unwrap_err().code, ErrorCode::InvalidUri);
176 }
177
178 #[test]
179 fn parses_grpc_cluster_with_explicit_ports() {
180 let target = parse("grpc://primary:5055,replica1:5055,replica2:5055").unwrap();
181 match target {
182 Target::GrpcCluster {
183 primary,
184 replicas,
185 force_primary,
186 } => {
187 assert_eq!(primary, "http://primary:5055");
188 assert_eq!(
189 replicas,
190 vec!["http://replica1:5055", "http://replica2:5055"]
191 );
192 assert!(!force_primary);
193 }
194 other => panic!("expected GrpcCluster, got {other:?}"),
195 }
196 }
197
198 #[test]
199 fn cluster_inherits_default_port_per_scheme() {
200 match parse("grpc://a,b").unwrap() {
201 Target::GrpcCluster {
202 primary, replicas, ..
203 } => {
204 assert_eq!(primary, "http://a:5055");
205 assert_eq!(replicas, vec!["http://b:5055"]);
206 }
207 other => panic!("expected GrpcCluster, got {other:?}"),
208 }
209 match parse("red://a,b").unwrap() {
210 Target::GrpcCluster {
211 primary, replicas, ..
212 } => {
213 assert_eq!(primary, "http://a:5050");
214 assert_eq!(replicas, vec!["http://b:5050"]);
215 }
216 other => panic!("expected GrpcCluster, got {other:?}"),
217 }
218 }
219
220 #[test]
221 fn cluster_per_host_port_overrides_default() {
222 match parse("grpc://a:7000,b:7001,c").unwrap() {
223 Target::GrpcCluster {
224 primary, replicas, ..
225 } => {
226 assert_eq!(primary, "http://a:7000");
227 assert_eq!(replicas, vec!["http://b:7001", "http://c:5055"]);
228 }
229 other => panic!("expected GrpcCluster, got {other:?}"),
230 }
231 }
232
233 #[test]
234 fn cluster_route_primary_query_param_forces_primary() {
235 match parse("grpc://primary,replica?route=primary").unwrap() {
236 Target::GrpcCluster {
237 primary,
238 replicas,
239 force_primary,
240 } => {
241 assert_eq!(primary, "http://primary:5055");
242 assert_eq!(replicas, vec!["http://replica:5055"]);
243 assert!(force_primary, "?route=primary must set force_primary");
244 }
245 other => panic!("expected GrpcCluster, got {other:?}"),
246 }
247 }
248
249 #[test]
250 fn cluster_rejects_empty_host_entry() {
251 assert_eq!(
252 parse("grpc://primary,,replica").unwrap_err().code,
253 ErrorCode::InvalidUri
254 );
255 assert_eq!(parse("grpc://,b").unwrap_err().code, ErrorCode::InvalidUri);
256 }
257
258 #[test]
259 fn cluster_rejects_invalid_port() {
260 assert_eq!(
261 parse("grpc://a:nope,b:5055").unwrap_err().code,
262 ErrorCode::InvalidUri
263 );
264 }
265
266 #[test]
267 fn single_host_grpc_still_routes_to_grpc_target_not_cluster() {
268 match parse("grpc://primary:5055").unwrap() {
269 Target::Grpc { endpoint } => assert_eq!(endpoint, "http://primary:5055"),
270 other => panic!("expected Grpc (single host), got {other:?}"),
271 }
272 }
273}