use crate::error::{MetadataError, Result};
use crate::iso19115::{Iso19115Metadata, ResponsibleParty, Role};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspireMetadata {
pub base: Iso19115Metadata,
pub resource_locator: Vec<ResourceLocator>,
pub unique_resource_identifier: Vec<UniqueResourceIdentifier>,
pub coupled_resource: Vec<CoupledResource>,
pub spatial_data_service_type: Option<SpatialDataServiceType>,
pub conformity: Vec<Conformity>,
pub conditions_for_access_and_use: Vec<String>,
pub limitations_on_public_access: Vec<String>,
pub responsible_organisation: Vec<ResponsibleParty>,
pub metadata_point_of_contact: ResponsibleParty,
pub metadata_date: chrono::DateTime<chrono::Utc>,
pub metadata_language: String,
}
pub struct InspireBuilder {
metadata: InspireMetadata,
}
impl InspireMetadata {
pub fn builder() -> InspireBuilder {
InspireBuilder {
metadata: Self::default(),
}
}
}
impl Default for InspireMetadata {
fn default() -> Self {
Self {
base: Iso19115Metadata::default(),
resource_locator: Vec::new(),
unique_resource_identifier: Vec::new(),
coupled_resource: Vec::new(),
spatial_data_service_type: None,
conformity: Vec::new(),
conditions_for_access_and_use: Vec::new(),
limitations_on_public_access: Vec::new(),
responsible_organisation: Vec::new(),
metadata_point_of_contact: ResponsibleParty {
individual_name: None,
organization_name: None,
position_name: None,
contact_info: None,
role: Role::PointOfContact,
},
metadata_date: chrono::Utc::now(),
metadata_language: "eng".to_string(),
}
}
}
impl InspireBuilder {
pub fn title(mut self, title: impl Into<String>) -> Self {
self.metadata.base = Iso19115Metadata::builder()
.title(title)
.build()
.unwrap_or_default();
self
}
pub fn resource_type(self, _resource_type: ResourceType) -> Self {
self
}
pub fn topic_category(self, _theme: InspireTheme) -> Self {
self
}
pub fn resource_locator(mut self, locator: ResourceLocator) -> Self {
self.metadata.resource_locator.push(locator);
self
}
pub fn unique_identifier(mut self, identifier: UniqueResourceIdentifier) -> Self {
self.metadata.unique_resource_identifier.push(identifier);
self
}
pub fn conformity(mut self, conformity: Conformity) -> Self {
self.metadata.conformity.push(conformity);
self
}
pub fn build(self) -> Result<InspireMetadata> {
if self.metadata.resource_locator.is_empty() {
return Err(MetadataError::ValidationError(
"At least one resource locator is required for INSPIRE".to_string(),
));
}
Ok(self.metadata)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLocator {
pub url: String,
pub description: Option<String>,
pub function: ResourceLocatorFunction,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ResourceLocatorFunction {
Download,
Information,
OfflineAccess,
Order,
Search,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniqueResourceIdentifier {
pub code: String,
pub code_space: Option<String>,
}
impl UniqueResourceIdentifier {
pub fn new(code: impl Into<String>) -> Self {
Self {
code: code.into(),
code_space: None,
}
}
pub fn with_code_space(code: impl Into<String>, code_space: impl Into<String>) -> Self {
Self {
code: code.into(),
code_space: Some(code_space.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoupledResource {
pub operation_name: String,
pub identifier: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SpatialDataServiceType {
Discovery,
View,
Download,
Transformation,
Invoke,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conformity {
pub specification: Specification,
pub degree: ConformityDegree,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Specification {
pub title: String,
pub date: chrono::DateTime<chrono::Utc>,
pub date_type: DateType,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum DateType {
Creation,
Publication,
Revision,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ConformityDegree {
Conformant,
NotConformant,
NotEvaluated,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum InspireTheme {
CoordinateReferenceSystems,
GeographicalGridSystems,
GeographicalNames,
AdministrativeUnits,
Addresses,
CadastralParcels,
TransportNetworks,
Hydrography,
ProtectedSites,
Elevation,
LandCover,
Orthoimagery,
Geology,
StatisticalUnits,
Buildings,
Soil,
LandUse,
HumanHealthAndSafety,
UtilityAndGovernmentalServices,
EnvironmentalMonitoringFacilities,
ProductionAndIndustrialFacilities,
AgriculturalAndAquacultureFacilities,
PopulationDistributionAndDemography,
AreaManagementRestrictionRegulationZones,
NaturalRiskZones,
AtmosphericConditions,
MeteorologicalGeographicalFeatures,
OceanographicGeographicalFeatures,
SeaRegions,
BioGeographicalRegions,
HabitatsAndBiotopes,
SpeciesDistribution,
EnergyResources,
MineralResources,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ResourceType {
Dataset,
DatasetSeries,
Service,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspireKeyword {
pub value: String,
pub originating_controlled_vocabulary: Option<String>,
}
impl InspireMetadata {
pub fn validate(&self) -> Result<ValidationReport> {
let mut report = ValidationReport::default();
if self.resource_locator.is_empty() {
report
.errors
.push("Resource locator is mandatory".to_string());
}
if self.unique_resource_identifier.is_empty() {
report
.errors
.push("Unique resource identifier is mandatory".to_string());
}
if self.conformity.is_empty() {
report
.warnings
.push("Conformity information is recommended".to_string());
}
if self.responsible_organisation.is_empty() {
report
.errors
.push("Responsible organisation is mandatory".to_string());
}
if self.conditions_for_access_and_use.is_empty() {
report
.errors
.push("Conditions for access and use are mandatory".to_string());
}
if self.limitations_on_public_access.is_empty() {
report
.errors
.push("Limitations on public access are mandatory".to_string());
}
report.is_valid = report.errors.is_empty();
Ok(report)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidationReport {
pub is_valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
#[cfg(feature = "xml")]
impl InspireMetadata {
pub fn to_xml(&self) -> Result<String> {
use quick_xml::se::to_string;
to_string(&self).map_err(|e| MetadataError::XmlError(e.to_string()))
}
pub fn from_xml(xml: &str) -> Result<Self> {
use quick_xml::de::from_str;
from_str(xml).map_err(|e| MetadataError::XmlError(e.to_string()))
}
}
impl InspireMetadata {
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(&self).map_err(|e| MetadataError::JsonError(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| MetadataError::JsonError(e.to_string()))
}
}