1use crate::capabilities::BackendCapabilities;
2use crate::errors::{EphemerisError, EphemerisErrorKind};
3use crate::identity::{AccuracyClass, BackendFamily, BackendId};
4use crate::metadata::{BackendMetadata, BackendProvenance};
5use crate::request::EphemerisRequest;
6use crate::result::EphemerisResult;
7use pleiades_types::{CelestialBody, Instant, TimeRange, TimeScale};
8
9pub trait EphemerisBackend: Send + Sync {
16 fn metadata(&self) -> BackendMetadata;
18
19 fn supports_body(&self, body: CelestialBody) -> bool;
21
22 fn position(&self, req: &EphemerisRequest) -> Result<EphemerisResult, EphemerisError>;
24
25 fn position_without_motion(
43 &self,
44 req: &EphemerisRequest,
45 ) -> Result<EphemerisResult, EphemerisError> {
46 let mut result = self.position(req)?;
47 result.motion = None;
48 Ok(result)
49 }
50
51 fn positions(&self, reqs: &[EphemerisRequest]) -> Result<Vec<EphemerisResult>, EphemerisError> {
58 reqs.iter().map(|req| self.position(req)).collect()
59 }
60}
61
62#[derive(Debug)]
67pub struct CompositeBackend<A, B> {
68 primary: A,
69 secondary: B,
70}
71
72impl<A, B> CompositeBackend<A, B> {
73 pub const fn new(primary: A, secondary: B) -> Self {
75 Self { primary, secondary }
76 }
77
78 pub const fn primary(&self) -> &A {
80 &self.primary
81 }
82
83 pub const fn secondary(&self) -> &B {
85 &self.secondary
86 }
87}
88
89impl<A: EphemerisBackend, B: EphemerisBackend> CompositeBackend<A, B> {
90 fn dispatch(
93 &self,
94 req: &EphemerisRequest,
95 query: impl Fn(&dyn EphemerisBackend) -> Result<EphemerisResult, EphemerisError>,
96 ) -> Result<EphemerisResult, EphemerisError> {
97 let primary_supports = self.primary.supports_body(req.body.clone());
98 let secondary_supports = self.secondary.supports_body(req.body.clone());
99
100 if primary_supports {
101 match query(&self.primary) {
102 Ok(result) => Ok(result),
103 Err(error) if secondary_supports && should_fallback_to_secondary(&error.kind) => {
104 query(&self.secondary)
105 }
106 Err(error) => Err(error),
107 }
108 } else if secondary_supports {
109 query(&self.secondary)
110 } else {
111 Err(EphemerisError::new(
112 EphemerisErrorKind::UnsupportedBody,
113 "no backend in the composite router supports the requested body",
114 ))
115 }
116 }
117}
118
119impl<A: EphemerisBackend, B: EphemerisBackend> EphemerisBackend for CompositeBackend<A, B> {
120 fn metadata(&self) -> BackendMetadata {
121 let primary = self.primary.metadata();
122 let secondary = self.secondary.metadata();
123 BackendMetadata {
124 id: BackendId::new(format!(
125 "composite:{}+{}",
126 primary.id.as_str(),
127 secondary.id.as_str()
128 )),
129 version: primary.version.clone(),
130 family: BackendFamily::Composite,
131 provenance: BackendProvenance {
132 summary: format!(
133 "Composite routing backend combining {} and {}.",
134 primary.provenance.summary, secondary.provenance.summary
135 ),
136 data_sources: combine_sources(
137 &primary.provenance.data_sources,
138 &secondary.provenance.data_sources,
139 ),
140 },
141 nominal_range: intersect_ranges(primary.nominal_range, secondary.nominal_range),
142 supported_time_scales: intersect_strings(
143 &primary.supported_time_scales,
144 &secondary.supported_time_scales,
145 ),
146 body_claims: crate::metadata::merge_body_claims(
147 &primary.body_claims,
148 &secondary.body_claims,
149 ),
150 supported_frames: intersect_strings(
151 &primary.supported_frames,
152 &secondary.supported_frames,
153 ),
154 capabilities: BackendCapabilities {
155 geocentric: primary.capabilities.geocentric && secondary.capabilities.geocentric,
156 topocentric: primary.capabilities.topocentric && secondary.capabilities.topocentric,
157 apparent: primary.capabilities.apparent && secondary.capabilities.apparent,
158 mean: primary.capabilities.mean && secondary.capabilities.mean,
159 batch: primary.capabilities.batch && secondary.capabilities.batch,
160 native_sidereal: primary.capabilities.native_sidereal
161 && secondary.capabilities.native_sidereal,
162 },
163 accuracy: min_accuracy(primary.accuracy, secondary.accuracy),
164 deterministic: primary.deterministic && secondary.deterministic,
165 offline: primary.offline && secondary.offline,
166 }
167 }
168
169 fn supports_body(&self, body: CelestialBody) -> bool {
170 self.primary.supports_body(body.clone()) || self.secondary.supports_body(body)
171 }
172
173 fn position(&self, req: &EphemerisRequest) -> Result<EphemerisResult, EphemerisError> {
174 self.dispatch(req, |backend| backend.position(req))
175 }
176
177 fn position_without_motion(
178 &self,
179 req: &EphemerisRequest,
180 ) -> Result<EphemerisResult, EphemerisError> {
181 self.dispatch(req, |backend| backend.position_without_motion(req))
182 }
183}
184
185#[derive(Default)]
192pub struct RoutingBackend {
193 backends: Vec<Box<dyn EphemerisBackend>>,
194}
195
196impl RoutingBackend {
197 pub fn new(backends: Vec<Box<dyn EphemerisBackend>>) -> Self {
199 Self { backends }
200 }
201
202 pub fn backends(&self) -> &[Box<dyn EphemerisBackend>] {
204 &self.backends
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.backends.is_empty()
210 }
211
212 fn dispatch(
215 &self,
216 req: &EphemerisRequest,
217 query: impl Fn(&dyn EphemerisBackend) -> Result<EphemerisResult, EphemerisError>,
218 ) -> Result<EphemerisResult, EphemerisError> {
219 let mut saw_support = false;
220 let mut last_retryable_error = None;
221
222 for backend in &self.backends {
223 if !backend.supports_body(req.body.clone()) {
224 continue;
225 }
226
227 saw_support = true;
228 match query(backend.as_ref()) {
229 Ok(result) => return Ok(result),
230 Err(error) if should_fallback_to_secondary(&error.kind) => {
231 last_retryable_error = Some(error);
232 }
233 Err(error) => return Err(error),
234 }
235 }
236
237 if let Some(error) = last_retryable_error {
238 Err(error)
239 } else if saw_support {
240 Err(EphemerisError::new(
241 EphemerisErrorKind::InvalidRequest,
242 "configured providers could not satisfy the requested body and request shape",
243 ))
244 } else {
245 Err(EphemerisError::new(
246 EphemerisErrorKind::UnsupportedBody,
247 "no backend in the routing chain supports the requested body",
248 ))
249 }
250 }
251}
252
253impl EphemerisBackend for RoutingBackend {
254 fn metadata(&self) -> BackendMetadata {
255 let backends: Vec<&dyn EphemerisBackend> = self
256 .backends
257 .iter()
258 .map(|backend| backend.as_ref())
259 .collect();
260 let metadatas: Vec<BackendMetadata> =
261 backends.iter().map(|backend| backend.metadata()).collect();
262
263 if metadatas.is_empty() {
264 return BackendMetadata {
265 id: BackendId::new("routing:empty"),
266 version: "routing[none]".to_string(),
267 family: BackendFamily::Composite,
268 provenance: BackendProvenance::new("Routing backend with no configured providers."),
269 nominal_range: TimeRange::new(None, None),
270 supported_time_scales: Vec::new(),
271 body_claims: Vec::new(),
272 supported_frames: Vec::new(),
273 capabilities: BackendCapabilities {
274 geocentric: false,
275 topocentric: false,
276 apparent: false,
277 mean: false,
278 batch: false,
279 native_sidereal: false,
280 },
281 accuracy: AccuracyClass::Unknown,
282 deterministic: true,
283 offline: true,
284 };
285 }
286
287 let mut id_parts = Vec::with_capacity(metadatas.len());
288 let mut version_parts = Vec::with_capacity(metadatas.len());
289 let mut provenance_parts = Vec::with_capacity(metadatas.len());
290 let mut data_sources = Vec::new();
291 let mut nominal_range = metadatas[0].nominal_range;
292 let mut supported_time_scales = metadatas[0].supported_time_scales.clone();
293 let mut body_claims = metadatas[0].body_claims.clone();
294 let mut supported_frames = metadatas[0].supported_frames.clone();
295 let mut capabilities = metadatas[0].capabilities.clone();
296 let mut accuracy = metadatas[0].accuracy;
297 let mut deterministic = metadatas[0].deterministic;
298 let mut offline = metadatas[0].offline;
299
300 for metadata in &metadatas {
301 id_parts.push(metadata.id.as_str().to_string());
302 version_parts.push(metadata.version.clone());
303 provenance_parts.push(metadata.provenance.summary.clone());
304 data_sources = combine_sources(&data_sources, &metadata.provenance.data_sources);
305 nominal_range = intersect_ranges(nominal_range, metadata.nominal_range);
306 supported_time_scales =
307 intersect_strings(&supported_time_scales, &metadata.supported_time_scales);
308 body_claims = crate::metadata::merge_body_claims(&body_claims, &metadata.body_claims);
309 supported_frames = intersect_strings(&supported_frames, &metadata.supported_frames);
310 capabilities.geocentric &= metadata.capabilities.geocentric;
311 capabilities.topocentric &= metadata.capabilities.topocentric;
312 capabilities.apparent &= metadata.capabilities.apparent;
313 capabilities.mean &= metadata.capabilities.mean;
314 capabilities.batch &= metadata.capabilities.batch;
315 capabilities.native_sidereal &= metadata.capabilities.native_sidereal;
316 accuracy = min_accuracy(accuracy, metadata.accuracy);
317 deterministic &= metadata.deterministic;
318 offline &= metadata.offline;
319 }
320
321 BackendMetadata {
322 id: BackendId::new(format!("routing:{}", id_parts.join("+"))),
323 version: format!("routing[{}]", version_parts.join("+")),
324 family: BackendFamily::Composite,
325 provenance: BackendProvenance {
326 summary: format!(
327 "Routing backend combining {} provider(s): {}.",
328 metadatas.len(),
329 provenance_parts.join("; ")
330 ),
331 data_sources,
332 },
333 nominal_range,
334 supported_time_scales,
335 body_claims,
336 supported_frames,
337 capabilities,
338 accuracy,
339 deterministic,
340 offline,
341 }
342 }
343
344 fn supports_body(&self, body: CelestialBody) -> bool {
345 self.backends
346 .iter()
347 .any(|backend| backend.supports_body(body.clone()))
348 }
349
350 fn position(&self, req: &EphemerisRequest) -> Result<EphemerisResult, EphemerisError> {
351 self.dispatch(req, |backend| backend.position(req))
352 }
353
354 fn position_without_motion(
355 &self,
356 req: &EphemerisRequest,
357 ) -> Result<EphemerisResult, EphemerisError> {
358 self.dispatch(req, |backend| backend.position_without_motion(req))
359 }
360}
361
362fn combine_sources(primary: &[String], secondary: &[String]) -> Vec<String> {
363 let mut combined = primary.to_vec();
364 for source in secondary {
365 if !combined.iter().any(|existing| existing == source) {
366 combined.push(source.clone());
367 }
368 }
369 combined
370}
371
372fn intersect_strings<T: Clone + PartialEq>(primary: &[T], secondary: &[T]) -> Vec<T> {
373 primary
374 .iter()
375 .filter(|value| secondary.contains(value))
376 .cloned()
377 .collect()
378}
379
380fn intersect_ranges(primary: TimeRange, secondary: TimeRange) -> TimeRange {
381 let start = match (primary.start, secondary.start) {
382 (Some(a), Some(b)) => Some(if a.julian_day.days() >= b.julian_day.days() {
383 a
384 } else {
385 b
386 }),
387 (Some(a), None) => Some(a),
388 (None, Some(b)) => Some(b),
389 (None, None) => None,
390 };
391 let end = match (primary.end, secondary.end) {
392 (Some(a), Some(b)) => Some(if a.julian_day.days() <= b.julian_day.days() {
393 a
394 } else {
395 b
396 }),
397 (Some(a), None) => Some(a),
398 (None, Some(b)) => Some(b),
399 (None, None) => None,
400 };
401
402 let canonical_scale = primary
403 .start
404 .or(primary.end)
405 .or(secondary.start)
406 .or(secondary.end)
407 .map(|instant| instant.scale);
408
409 TimeRange::new(
410 start.map(|instant| retag_instant(instant, canonical_scale)),
411 end.map(|instant| retag_instant(instant, canonical_scale)),
412 )
413}
414
415fn retag_instant(instant: Instant, scale: Option<TimeScale>) -> Instant {
416 match scale {
417 Some(scale) if instant.scale != scale => Instant::new(instant.julian_day, scale),
418 _ => instant,
419 }
420}
421
422fn min_accuracy(primary: AccuracyClass, secondary: AccuracyClass) -> AccuracyClass {
423 use AccuracyClass::*;
424
425 match (primary, secondary) {
426 (Unknown, _) | (_, Unknown) => Unknown,
427 (Approximate, _) | (_, Approximate) => Approximate,
428 (Moderate, _) | (_, Moderate) => Moderate,
429 (High, _) | (_, High) => High,
430 (Exact, Exact) => Exact,
431 }
432}
433
434fn should_fallback_to_secondary(kind: &EphemerisErrorKind) -> bool {
435 matches!(
436 kind,
437 EphemerisErrorKind::UnsupportedBody
438 | EphemerisErrorKind::UnsupportedCoordinateFrame
439 | EphemerisErrorKind::UnsupportedTimeScale
440 | EphemerisErrorKind::InvalidObserver
441 | EphemerisErrorKind::UnsupportedObserver
442 | EphemerisErrorKind::MissingDataset
443 | EphemerisErrorKind::UnsupportedApparentness
444 | EphemerisErrorKind::UnsupportedZodiacMode
445 | EphemerisErrorKind::InvalidRequest
446 )
447}
448
449#[cfg(test)]
450#[path = "traits_tests.rs"]
451mod tests;