1use serde::{Deserialize, Serialize};
2use std::{
3 collections::BTreeMap,
4 error::Error,
5 fmt,
6 sync::{Arc, RwLock},
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct ServiceReference(String);
14
15impl ServiceReference {
16 #[must_use]
17 pub fn new(value: impl Into<String>) -> Self {
18 Self(value.into())
19 }
20
21 #[must_use]
22 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct Endpoint {
30 pub address: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub operating_region: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub failure_domain: Option<String>,
35}
36
37impl Endpoint {
38 #[must_use]
39 pub fn new(address: impl Into<String>) -> Self {
40 Self {
41 address: address.into(),
42 operating_region: None,
43 failure_domain: None,
44 }
45 }
46
47 #[must_use]
48 pub fn in_region(mut self, region: impl Into<String>) -> Self {
49 self.operating_region = Some(region.into());
50 self
51 }
52
53 #[must_use]
54 pub fn in_failure_domain(mut self, failure_domain: impl Into<String>) -> Self {
55 self.failure_domain = Some(failure_domain.into());
56 self
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct EndpointState {
63 pub service: ServiceReference,
64 pub endpoints: Vec<Endpoint>,
65}
66
67impl EndpointState {
68 #[must_use]
69 pub fn new(service: ServiceReference, endpoints: Vec<Endpoint>) -> Self {
70 Self { service, endpoints }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum EndpointResolutionErrorCode {
77 InvalidEndpointState,
78 SourceUnavailable,
79 NoUsableEndpointState,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct EndpointResolutionError {
85 pub code: EndpointResolutionErrorCode,
86 pub message: String,
87 pub next_action: String,
88}
89
90impl EndpointResolutionError {
91 #[must_use]
92 pub fn source_unavailable(service: &ServiceReference, message: impl Into<String>) -> Self {
93 Self {
94 code: EndpointResolutionErrorCode::SourceUnavailable,
95 message: message.into(),
96 next_action: format!(
97 "Restore the endpoint source for Service Reference `{}`, then retry.",
98 service.as_str()
99 ),
100 }
101 }
102
103 fn no_usable_state(service: &ServiceReference) -> Self {
104 Self {
105 code: EndpointResolutionErrorCode::NoUsableEndpointState,
106 message: format!(
107 "No usable endpoint state exists for Service Reference `{}`.",
108 service.as_str()
109 ),
110 next_action: format!(
111 "Configure or publish at least one endpoint for Service Reference `{}`, then retry.",
112 service.as_str()
113 ),
114 }
115 }
116
117 fn invalid_state(message: impl Into<String>) -> Self {
118 Self {
119 code: EndpointResolutionErrorCode::InvalidEndpointState,
120 message: message.into(),
121 next_action: "Publish a non-empty endpoint state whose Service Reference matches its registry key."
122 .to_owned(),
123 }
124 }
125}
126
127impl fmt::Display for EndpointResolutionError {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(formatter, "{}", self.message)
130 }
131}
132
133impl Error for EndpointResolutionError {}
134
135pub trait EndpointResolver {
136 fn resolve(&self, service: &ServiceReference)
137 -> Result<EndpointState, EndpointResolutionError>;
138}
139
140#[derive(Debug, Clone)]
141pub struct StaticEndpointResolver {
142 states: BTreeMap<ServiceReference, EndpointState>,
143}
144
145impl StaticEndpointResolver {
146 pub fn new(
147 states: impl IntoIterator<Item = EndpointState>,
148 ) -> Result<Self, EndpointResolutionError> {
149 let mut configured = BTreeMap::new();
150 for state in states {
151 validate_state(&state)?;
152 let service = state.service.clone();
153 if configured.insert(service.clone(), state).is_some() {
154 return Err(EndpointResolutionError::invalid_state(format!(
155 "Static endpoint configuration contains Service Reference `{}` more than once.",
156 service.as_str()
157 )));
158 }
159 }
160 Ok(Self { states: configured })
161 }
162}
163
164impl EndpointResolver for StaticEndpointResolver {
165 fn resolve(
166 &self,
167 service: &ServiceReference,
168 ) -> Result<EndpointState, EndpointResolutionError> {
169 self.states
170 .get(service)
171 .cloned()
172 .ok_or_else(|| EndpointResolutionError::no_usable_state(service))
173 }
174}
175
176#[derive(Debug, Clone, Default)]
177pub struct LocalProcessEndpointResolver {
180 states: Arc<RwLock<BTreeMap<ServiceReference, EndpointState>>>,
181}
182
183impl LocalProcessEndpointResolver {
184 #[must_use]
185 pub fn new() -> Self {
186 Self::default()
187 }
188
189 pub fn publish(&self, state: EndpointState) -> Result<(), EndpointResolutionError> {
190 validate_state(&state)?;
191 self.states
192 .write()
193 .expect("local process endpoint registry lock poisoned")
194 .insert(state.service.clone(), state);
195 Ok(())
196 }
197
198 pub fn remove(&self, service: &ServiceReference) -> Option<EndpointState> {
199 self.states
200 .write()
201 .expect("local process endpoint registry lock poisoned")
202 .remove(service)
203 }
204}
205
206impl EndpointResolver for LocalProcessEndpointResolver {
207 fn resolve(
208 &self,
209 service: &ServiceReference,
210 ) -> Result<EndpointState, EndpointResolutionError> {
211 self.states
212 .read()
213 .expect("local process endpoint registry lock poisoned")
214 .get(service)
215 .cloned()
216 .ok_or_else(|| EndpointResolutionError::no_usable_state(service))
217 }
218}
219
220#[derive(Debug, Clone)]
221pub struct LastValidEndpointResolver<R> {
224 source: R,
225 last_valid: Arc<RwLock<BTreeMap<ServiceReference, EndpointState>>>,
226}
227
228impl<R> LastValidEndpointResolver<R> {
229 #[must_use]
230 pub fn new(source: R) -> Self {
231 Self {
232 source,
233 last_valid: Arc::new(RwLock::new(BTreeMap::new())),
234 }
235 }
236}
237
238impl<R: EndpointResolver> EndpointResolver for LastValidEndpointResolver<R> {
239 fn resolve(
240 &self,
241 service: &ServiceReference,
242 ) -> Result<EndpointState, EndpointResolutionError> {
243 match self.source.resolve(service) {
244 Ok(state) => {
245 if validate_state_for(service, &state).is_ok() {
246 self.last_valid
247 .write()
248 .expect("last valid endpoint state lock poisoned")
249 .insert(service.clone(), state.clone());
250 Ok(state)
251 } else {
252 self.cached_or_unavailable(service)
253 }
254 }
255 Err(_) => self.cached_or_unavailable(service),
256 }
257 }
258}
259
260impl<R> LastValidEndpointResolver<R> {
261 fn cached_or_unavailable(
262 &self,
263 service: &ServiceReference,
264 ) -> Result<EndpointState, EndpointResolutionError> {
265 self.last_valid
266 .read()
267 .expect("last valid endpoint state lock poisoned")
268 .get(service)
269 .cloned()
270 .ok_or_else(|| EndpointResolutionError::no_usable_state(service))
271 }
272}
273
274fn validate_state(state: &EndpointState) -> Result<(), EndpointResolutionError> {
275 validate_state_for(&state.service, state)
276}
277
278fn validate_state_for(
279 service: &ServiceReference,
280 state: &EndpointState,
281) -> Result<(), EndpointResolutionError> {
282 if state.service != *service {
283 return Err(EndpointResolutionError::invalid_state(
284 "Endpoint state Service Reference does not match the requested Service Reference.",
285 ));
286 }
287 if service.as_str().trim().is_empty() || state.endpoints.is_empty() {
288 return Err(EndpointResolutionError::invalid_state(
289 "Endpoint state requires a non-empty Service Reference and at least one endpoint.",
290 ));
291 }
292 if state
293 .endpoints
294 .iter()
295 .any(|endpoint| endpoint.address.trim().is_empty())
296 {
297 return Err(EndpointResolutionError::invalid_state(
298 "Endpoint addresses must not be empty.",
299 ));
300 }
301 Ok(())
302}