1use std::collections::HashMap;
2use std::env;
3
4use crate::error::{Error, Result};
5
6pub const DEFAULT_GROUP: &str = "DEFAULT_GROUP";
8
9pub const DEFAULT_WEIGHT: f64 = 1.0;
11
12pub const META_GRPC_PORT: &str = "gRPC_port";
14
15pub mod env_keys {
17 pub const NACOS_ADDR: &str = "NACOS_ADDR";
19 pub const NACOS_NAMESPACE: &str = "NACOS_NAMESPACE";
21 pub const NACOS_USERNAME: &str = "NACOS_USERNAME";
23 pub const NACOS_PASSWORD: &str = "NACOS_PASSWORD";
25 pub const SERVICE_ADDR: &str = "SERVICE_ADDR";
27 pub const SERVICE_NAME: &str = "SERVICE_NAME";
29 pub const SERVICE_HOST: &str = "SERVICE_HOST";
31}
32
33#[derive(Debug, Clone)]
37pub struct ServiceConfig {
38 pub nacos_addr: String,
40 pub namespace: String,
42 pub service_name: String,
44 pub group: String,
46 pub service_host: String,
48 pub service_port: u16,
50 pub weight: f64,
52 pub ephemeral: bool,
54 pub auth: Option<(String, String)>,
56 pub metadata: HashMap<String, String>,
59}
60
61impl ServiceConfig {
62 pub fn builder() -> ServiceConfigBuilder {
64 ServiceConfigBuilder::default()
65 }
66
67 pub fn from_env() -> Result<Self> {
76 let nacos_addr = read_env(env_keys::NACOS_ADDR)?;
77 let namespace = read_env(env_keys::NACOS_NAMESPACE)?;
78 let service_addr = read_env(env_keys::SERVICE_ADDR)?;
79 let service_name = read_env(env_keys::SERVICE_NAME)?;
80 let service_host = env::var(env_keys::SERVICE_HOST).ok();
81 let username = env::var(env_keys::NACOS_USERNAME).ok();
82 let password = env::var(env_keys::NACOS_PASSWORD).ok();
83
84 let mut builder = Self::builder()
85 .nacos_addr(nacos_addr)
86 .namespace(namespace)
87 .service_name(service_name)
88 .bind_addr(service_addr)?;
89 if let Some(host) = service_host {
90 builder = builder.service_host(host);
91 }
92 match (username, password) {
93 (Some(u), Some(p)) => builder = builder.auth(u, p),
94 (None, None) => {}
95 _ => {
96 return Err(Error::invalid_config(
97 "NACOS_USERNAME and NACOS_PASSWORD must be provided together",
98 ));
99 }
100 }
101 builder.build()
102 }
103}
104
105#[derive(Debug, Default, Clone)]
107pub struct ServiceConfigBuilder {
108 nacos_addr: Option<String>,
109 namespace: Option<String>,
110 service_name: Option<String>,
111 group: Option<String>,
112 service_host: Option<String>,
113 service_port: Option<u16>,
114 weight: Option<f64>,
115 ephemeral: Option<bool>,
116 auth: Option<(String, String)>,
117 metadata: HashMap<String, String>,
118}
119
120impl ServiceConfigBuilder {
121 pub fn nacos_addr(mut self, addr: impl Into<String>) -> Self {
123 self.nacos_addr = Some(addr.into());
124 self
125 }
126
127 pub fn namespace(mut self, ns: impl Into<String>) -> Self {
129 self.namespace = Some(ns.into());
130 self
131 }
132
133 pub fn service_name(mut self, name: impl Into<String>) -> Self {
135 self.service_name = Some(name.into());
136 self
137 }
138
139 pub fn group(mut self, group: impl Into<String>) -> Self {
141 self.group = Some(group.into());
142 self
143 }
144
145 pub fn service_host(mut self, host: impl Into<String>) -> Self {
147 self.service_host = Some(host.into());
148 self
149 }
150
151 pub fn service_port(mut self, port: u16) -> Self {
153 self.service_port = Some(port);
154 self
155 }
156
157 pub fn bind_addr(mut self, addr: impl AsRef<str>) -> Result<Self> {
162 let addr = addr.as_ref();
163 let (host, port) = addr.rsplit_once(':').ok_or_else(|| {
164 Error::invalid_config(format!("invalid bind address `{addr}` (expect host:port)"))
165 })?;
166 if host.is_empty() {
167 return Err(Error::invalid_config(format!(
168 "invalid bind address `{addr}`: empty host"
169 )));
170 }
171 let port: u16 = port.parse().map_err(|_| {
172 Error::invalid_config(format!("invalid bind address `{addr}`: bad port"))
173 })?;
174 self.service_port = Some(port);
175 Ok(self)
176 }
177
178 pub fn weight(mut self, weight: f64) -> Self {
180 self.weight = Some(weight);
181 self
182 }
183
184 pub fn ephemeral(mut self, ephemeral: bool) -> Self {
186 self.ephemeral = Some(ephemeral);
187 self
188 }
189
190 pub fn auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
192 self.auth = Some((username.into(), password.into()));
193 self
194 }
195
196 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
198 self.metadata.insert(key.into(), value.into());
199 self
200 }
201
202 pub fn metadata_all<I, K, V>(mut self, entries: I) -> Self
204 where
205 I: IntoIterator<Item = (K, V)>,
206 K: Into<String>,
207 V: Into<String>,
208 {
209 self.metadata
210 .extend(entries.into_iter().map(|(k, v)| (k.into(), v.into())));
211 self
212 }
213
214 pub fn build(self) -> Result<ServiceConfig> {
216 let nacos_addr = require(self.nacos_addr, "nacos_addr")?;
217 validate_host_port(&nacos_addr, "nacos_addr")?;
218 let namespace = require(self.namespace, "namespace")?;
219 let service_name = require(self.service_name, "service_name")?;
220 let service_port = require(self.service_port, "service_port")?;
221 let service_host = match self.service_host {
222 Some(h) => h,
223 None => local_ip_address::local_ip()?.to_string(),
224 };
225 let mut metadata = self.metadata;
226 metadata
227 .entry(META_GRPC_PORT.to_string())
228 .or_insert_with(|| service_port.to_string());
229
230 Ok(ServiceConfig {
231 nacos_addr,
232 namespace,
233 service_name,
234 group: self.group.unwrap_or_else(|| DEFAULT_GROUP.to_string()),
235 service_host,
236 service_port,
237 weight: self.weight.unwrap_or(DEFAULT_WEIGHT),
238 ephemeral: self.ephemeral.unwrap_or(true),
239 auth: self.auth,
240 metadata,
241 })
242 }
243}
244
245fn read_env(name: &str) -> Result<String> {
247 env::var(name).map_err(|source| Error::Env {
248 name: name.to_string(),
249 source,
250 })
251}
252
253fn require<T>(value: Option<T>, field: &str) -> Result<T> {
255 value.ok_or_else(|| Error::invalid_config(format!("missing required field `{field}`")))
256}
257
258fn validate_host_port(addr: &str, field: &str) -> Result<()> {
261 let (host, port) = addr.rsplit_once(':').ok_or_else(|| {
262 Error::invalid_config(format!("invalid `{field}` = `{addr}` (expect host:port)"))
263 })?;
264 if host.is_empty() {
265 return Err(Error::invalid_config(format!(
266 "invalid `{field}` = `{addr}`: empty host"
267 )));
268 }
269 port.parse::<u16>()
270 .map_err(|_| Error::invalid_config(format!("invalid `{field}` = `{addr}`: bad port")))?;
271 Ok(())
272}
273
274#[cfg(test)]
275#[allow(clippy::unwrap_used, clippy::expect_used)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn builder_requires_mandatory_fields() {
281 let err = ServiceConfig::builder().build().unwrap_err();
282 assert!(matches!(err, Error::InvalidConfig(_)));
283 }
284
285 #[test]
286 fn builder_bind_addr_extracts_port() {
287 let cfg = ServiceConfig::builder()
288 .nacos_addr("127.0.0.1:8848")
289 .namespace("public")
290 .service_name("svc")
291 .bind_addr("0.0.0.0:9000")
292 .unwrap()
293 .service_host("10.0.0.1")
294 .build()
295 .unwrap();
296 assert_eq!(cfg.service_port, 9000);
297 assert_eq!(cfg.service_host, "10.0.0.1");
298 assert_eq!(
299 cfg.metadata.get(META_GRPC_PORT).map(String::as_str),
300 Some("9000")
301 );
302 }
303
304 #[test]
305 fn builder_rejects_bad_bind_addr() {
306 let err = ServiceConfig::builder()
307 .bind_addr("no-port-here")
308 .unwrap_err();
309 assert!(matches!(err, Error::InvalidConfig(_)));
310
311 let err = ServiceConfig::builder().bind_addr(":9000").unwrap_err();
312 assert!(matches!(err, Error::InvalidConfig(_)));
313
314 let err = ServiceConfig::builder()
315 .bind_addr("host:not-a-port")
316 .unwrap_err();
317 assert!(matches!(err, Error::InvalidConfig(_)));
318 }
319
320 #[test]
321 fn validate_host_port_accepts_hostname() {
322 assert!(validate_host_port("nacos.internal:8848", "nacos_addr").is_ok());
323 assert!(validate_host_port("localhost:8848", "nacos_addr").is_ok());
324 assert!(validate_host_port("127.0.0.1:8848", "nacos_addr").is_ok());
325 }
326
327 #[test]
328 fn validate_host_port_rejects_empty_host() {
329 assert!(validate_host_port(":8848", "nacos_addr").is_err());
330 }
331
332 #[test]
333 fn metadata_user_override_takes_precedence() {
334 let cfg = ServiceConfig::builder()
335 .nacos_addr("127.0.0.1:8848")
336 .namespace("public")
337 .service_name("svc")
338 .service_host("1.2.3.4")
339 .service_port(9000)
340 .metadata(META_GRPC_PORT, "custom")
341 .build()
342 .unwrap();
343 assert_eq!(
344 cfg.metadata.get(META_GRPC_PORT).map(String::as_str),
345 Some("custom")
346 );
347 }
348}