use crate::capabilities::{BackendCapabilities, BackendCapabilitiesValidationError};
use crate::errors::{format_display_list, EphemerisError, EphemerisErrorKind};
use crate::identity::{AccuracyClass, BackendFamily, BackendId};
use crate::request::EphemerisRequest;
use crate::validation::{validate_non_blank, validate_non_empty_unique, validate_unique_entries};
use core::fmt;
use pleiades_types::{
CelestialBody, CoordinateFrame, TimeRange, TimeRangeValidationError, TimeScale, ZodiacMode,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BackendProvenance {
pub summary: String,
pub data_sources: Vec<String>,
}
impl BackendProvenance {
pub fn new(summary: impl Into<String>) -> Self {
Self {
summary: summary.into(),
data_sources: Vec::new(),
}
}
pub fn summary_line(&self) -> String {
self.summary.clone()
}
pub fn validate(&self) -> Result<(), BackendProvenanceValidationError> {
validate_non_blank("provenance summary", &self.summary)
.map_err(|_| BackendProvenanceValidationError::BlankSummary)?;
for (index, source) in self.data_sources.iter().enumerate() {
if source.trim().is_empty() || source.trim() != source {
return Err(BackendProvenanceValidationError::BlankDataSource { index });
}
}
validate_unique_entries("provenance data sources", &self.data_sources).map_err(|error| {
match error {
BackendMetadataValidationError::DuplicateEntry { value, .. } => {
BackendProvenanceValidationError::DuplicateDataSource { value }
}
_ => {
unreachable!("duplicate provenance sources should only fail via DuplicateEntry")
}
}
})
}
pub fn validated_summary_line(&self) -> Result<String, BackendProvenanceValidationError> {
self.validate()?;
Ok(self.summary_line())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BackendProvenanceValidationError {
BlankSummary,
BlankDataSource {
index: usize,
},
DuplicateDataSource {
value: String,
},
}
impl BackendProvenanceValidationError {
pub fn summary_line(&self) -> String {
match self {
Self::BlankSummary => {
"backend provenance summary must not be blank or whitespace-padded".to_owned()
}
Self::BlankDataSource { index } => format!(
"backend provenance data source at index {index} must not be blank or whitespace-padded"
),
Self::DuplicateDataSource { value } => {
format!("backend provenance data sources contain duplicate entry `{value}`")
}
}
}
}
impl fmt::Display for BackendProvenanceValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
impl std::error::Error for BackendProvenanceValidationError {}
impl fmt::Display for BackendProvenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct BackendMetadata {
pub id: BackendId,
pub version: String,
pub family: BackendFamily,
pub provenance: BackendProvenance,
pub nominal_range: TimeRange,
pub supported_time_scales: Vec<TimeScale>,
pub body_coverage: Vec<CelestialBody>,
pub supported_frames: Vec<CoordinateFrame>,
pub capabilities: BackendCapabilities,
pub accuracy: AccuracyClass,
pub deterministic: bool,
pub offline: bool,
}
impl BackendMetadata {
pub fn summary_line(&self) -> String {
format!(
"id={}; version={}; family={}; family posture={}; accuracy={}; deterministic={}; offline={}; nominal range={}; time scales=[{}]; bodies=[{}]; frames=[{}]; capabilities=[{}]; provenance={}",
self.id,
self.version,
self.family,
self.family.posture_label(),
self.accuracy,
self.deterministic,
self.offline,
self.nominal_range,
format_display_list(&self.supported_time_scales),
format_display_list(&self.body_coverage),
format_display_list(&self.supported_frames),
self.capabilities.summary_line(),
self.provenance.summary_line(),
)
}
pub fn validated_summary_line(&self) -> Result<String, BackendMetadataValidationError> {
self.validate()?;
Ok(self.summary_line())
}
pub fn validate_request(&self, req: &EphemerisRequest) -> Result<(), EphemerisError> {
req.validate_custom_definitions()?;
if !self.family.is_routing() {
crate::policy::current::validate_request_policy(
req,
self.id.as_str(),
&self.supported_time_scales,
&self.supported_frames,
self.capabilities.mean,
self.capabilities.apparent,
)?;
if !self.capabilities.native_sidereal {
crate::policy::current::validate_zodiac_policy(
req,
self.id.as_str(),
&[ZodiacMode::Tropical],
)?;
}
crate::policy::current::validate_request_observer_location(req)?;
crate::policy::current::validate_observer_policy(
req,
self.id.as_str(),
self.capabilities.topocentric,
)?;
} else {
crate::policy::current::validate_request_observer_location(req)?;
}
if !self.body_coverage.contains(&req.body) {
return Err(EphemerisError::new(
EphemerisErrorKind::UnsupportedBody,
format!("{} does not support {}", self.id, req.body),
));
}
Ok(())
}
}
impl fmt::Display for BackendMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BackendMetadataValidationError {
BlankField { field: &'static str },
EmptyField { field: &'static str },
DuplicateEntry { field: &'static str, value: String },
NominalRangeNotFinite,
NominalRangeScaleMismatch,
NominalRangeOutOfOrder,
InvalidCapabilities {
field: &'static str,
message: &'static str,
},
}
impl BackendMetadataValidationError {
pub fn summary_line(&self) -> String {
match self {
Self::BlankField { field } => {
format!("backend metadata field `{field}` is blank or whitespace-padded")
}
Self::EmptyField { field } => {
format!("backend metadata field `{field}` must not be empty")
}
Self::DuplicateEntry { field, value } => {
format!("backend metadata field `{field}` contains duplicate entry `{value}`")
}
Self::NominalRangeNotFinite => {
"backend metadata nominal range must use finite Julian-day bounds".to_owned()
}
Self::NominalRangeScaleMismatch => {
"backend metadata nominal range bounds must use the same time scale".to_owned()
}
Self::NominalRangeOutOfOrder => {
"backend metadata nominal range end must not precede the start".to_owned()
}
Self::InvalidCapabilities { field, message } => {
format!("backend metadata field `{field}` is invalid: {message}")
}
}
}
}
impl fmt::Display for BackendMetadataValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
impl std::error::Error for BackendMetadataValidationError {}
impl BackendMetadata {
pub fn validate(&self) -> Result<(), BackendMetadataValidationError> {
validate_non_blank("id", self.id.as_str())?;
validate_non_blank("version", &self.version)?;
self.provenance.validate().map_err(|error| match error {
BackendProvenanceValidationError::BlankSummary => {
BackendMetadataValidationError::BlankField {
field: "provenance summary",
}
}
BackendProvenanceValidationError::BlankDataSource { .. } => {
BackendMetadataValidationError::BlankField {
field: "provenance data sources",
}
}
BackendProvenanceValidationError::DuplicateDataSource { value } => {
BackendMetadataValidationError::DuplicateEntry {
field: "provenance data sources",
value,
}
}
})?;
validate_non_empty_unique("supported time scales", &self.supported_time_scales)?;
validate_non_empty_unique("body coverage", &self.body_coverage)?;
validate_non_empty_unique("supported frames", &self.supported_frames)?;
self.capabilities.validate().map_err(|error| match error {
BackendCapabilitiesValidationError::MissingPositionMode => {
BackendMetadataValidationError::InvalidCapabilities {
field: "capabilities",
message: error.summary_line(),
}
}
BackendCapabilitiesValidationError::MissingValueMode => {
BackendMetadataValidationError::InvalidCapabilities {
field: "capabilities",
message: error.summary_line(),
}
}
})?;
self.validate_nominal_range()?;
Ok(())
}
fn validate_nominal_range(&self) -> Result<(), BackendMetadataValidationError> {
match self.nominal_range.validate() {
Ok(()) => Ok(()),
Err(TimeRangeValidationError::NonFiniteBound { .. }) => {
Err(BackendMetadataValidationError::NominalRangeNotFinite)
}
Err(TimeRangeValidationError::ScaleMismatch { .. }) => {
Err(BackendMetadataValidationError::NominalRangeScaleMismatch)
}
Err(TimeRangeValidationError::OutOfOrder { .. }) => {
Err(BackendMetadataValidationError::NominalRangeOutOfOrder)
}
}
}
}
#[cfg(test)]
#[path = "metadata_tests.rs"]
mod tests;