use std::fmt;
use crate::types::GeometryType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtensionScope {
ReadWrite,
WriteOnly,
Other(String),
}
impl ExtensionScope {
pub fn parse(value: &str) -> Self {
match value {
"read-write" => Self::ReadWrite,
"write-only" => Self::WriteOnly,
other => Self::Other(other.to_owned()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::ReadWrite => "read-write",
Self::WriteOnly => "write-only",
Self::Other(value) => value,
}
}
pub fn affects_readers(&self) -> bool {
!matches!(self, Self::WriteOnly)
}
pub fn affects_writers(&self) -> bool {
true
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Extension {
GeometryType(GeometryType),
RtreeIndex,
GeometryTypeTrigger,
SrsIdTrigger,
ZoomOther,
Webp,
Metadata,
Schema,
CrsWkt,
CrsWkt11,
GriddedCoverage,
RelatedTables,
GdalAspatial,
Other(String),
}
const GEOM_PREFIX: &str = "gpkg_geom_";
pub const GEOM_TYPE_EXTENSION_DEFINITION: &str =
"http://www.geopackage.org/spec140/#extension_geometry_types";
impl Extension {
pub fn from_name(name: &str) -> Self {
if let Some(geometry_type) = name.strip_prefix(GEOM_PREFIX) {
return match GeometryType::parse(geometry_type) {
Some(parsed) if parsed.is_extension() => Self::GeometryType(parsed),
_ => Self::Other(name.to_owned()),
};
}
match name {
crate::triggers::EXTENSION_NAME => Self::RtreeIndex,
"gpkg_geometry_type_trigger" => Self::GeometryTypeTrigger,
"gpkg_srs_id_trigger" => Self::SrsIdTrigger,
crate::tiles::ZOOM_OTHER_EXTENSION_NAME => Self::ZoomOther,
crate::tiles::WEBP_EXTENSION_NAME => Self::Webp,
"gpkg_metadata" => Self::Metadata,
"gpkg_schema" => Self::Schema,
"gpkg_crs_wkt" => Self::CrsWkt,
"gpkg_crs_wkt_1_1" => Self::CrsWkt11,
"gpkg_2d_gridded_coverage" | "2d_gridded_coverage" | "gpkg_elevation_tiles" => {
Self::GriddedCoverage
}
"related_tables" | "gpkg_related_tables" => Self::RelatedTables,
"gdal_aspatial" => Self::GdalAspatial,
other => Self::Other(other.to_owned()),
}
}
pub fn name(&self) -> String {
match self {
Self::GeometryType(geometry_type) => {
format!("{GEOM_PREFIX}{}", geometry_type.as_str())
}
Self::RtreeIndex => crate::triggers::EXTENSION_NAME.to_owned(),
Self::GeometryTypeTrigger => "gpkg_geometry_type_trigger".to_owned(),
Self::SrsIdTrigger => "gpkg_srs_id_trigger".to_owned(),
Self::ZoomOther => crate::tiles::ZOOM_OTHER_EXTENSION_NAME.to_owned(),
Self::Webp => crate::tiles::WEBP_EXTENSION_NAME.to_owned(),
Self::Metadata => "gpkg_metadata".to_owned(),
Self::Schema => "gpkg_schema".to_owned(),
Self::CrsWkt => "gpkg_crs_wkt".to_owned(),
Self::CrsWkt11 => "gpkg_crs_wkt_1_1".to_owned(),
Self::GriddedCoverage => "gpkg_2d_gridded_coverage".to_owned(),
Self::RelatedTables => crate::related::EXTENSION_NAME.to_owned(),
Self::GdalAspatial => "gdal_aspatial".to_owned(),
Self::Other(name) => name.clone(),
}
}
pub fn support(&self) -> ExtensionSupport {
match self {
Self::RtreeIndex
| Self::ZoomOther
| Self::Webp
| Self::CrsWkt
| Self::CrsWkt11
| Self::GeometryType(_)
| Self::Metadata
| Self::Schema
| Self::RelatedTables => ExtensionSupport::Implemented,
Self::GriddedCoverage | Self::GdalAspatial => ExtensionSupport::Known,
Self::GeometryTypeTrigger | Self::SrsIdTrigger => ExtensionSupport::Removed,
Self::Other(_) => ExtensionSupport::Unrecognised,
}
}
pub fn is_removed(&self) -> bool {
self.support() == ExtensionSupport::Removed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExtensionSupport {
Implemented,
Known,
Removed,
Unrecognised,
}
impl ExtensionSupport {
pub fn as_str(self) -> &'static str {
match self {
Self::Implemented => "implemented",
Self::Known => "known, not read or written",
Self::Removed => "removed from the standard in 2016",
Self::Unrecognised => "unrecognised",
}
}
}
impl fmt::Display for ExtensionSupport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Display for ExtensionScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_round_trip() {
for extension in [
Extension::GeometryType(GeometryType::CircularString),
Extension::RtreeIndex,
Extension::GeometryTypeTrigger,
Extension::SrsIdTrigger,
Extension::ZoomOther,
Extension::Webp,
Extension::Metadata,
Extension::Schema,
Extension::CrsWkt,
Extension::CrsWkt11,
Extension::GriddedCoverage,
Extension::RelatedTables,
Extension::GdalAspatial,
Extension::Other("acme_something".to_owned()),
] {
assert_eq!(Extension::from_name(&extension.name()), extension);
}
}
#[test]
fn historical_spellings_map_to_the_current_extension() {
for name in [
"gpkg_elevation_tiles",
"2d_gridded_coverage",
"gpkg_2d_gridded_coverage",
] {
assert_eq!(
Extension::from_name(name),
Extension::GriddedCoverage,
"{name}"
);
}
for name in ["related_tables", "gpkg_related_tables"] {
assert_eq!(
Extension::from_name(name),
Extension::RelatedTables,
"{name}"
);
}
}
#[test]
fn geometry_type_names_need_an_extension_type() {
assert_eq!(
Extension::from_name("gpkg_geom_CIRCULARSTRING"),
Extension::GeometryType(GeometryType::CircularString)
);
assert_eq!(
Extension::from_name("gpkg_geom_POINT"),
Extension::Other("gpkg_geom_POINT".to_owned())
);
assert_eq!(
Extension::from_name("gpkg_geom_TIN"),
Extension::Other("gpkg_geom_TIN".to_owned())
);
}
#[test]
fn scope_values_outside_requirement_64_are_kept_as_written() {
assert_eq!(
ExtensionScope::parse("read-write"),
ExtensionScope::ReadWrite
);
assert_eq!(
ExtensionScope::parse("write-only"),
ExtensionScope::WriteOnly
);
let shouty = ExtensionScope::parse("Read-Write");
assert_eq!(shouty, ExtensionScope::Other("Read-Write".to_owned()));
assert_eq!(shouty.as_str(), "Read-Write");
assert!(
shouty.affects_readers(),
"an undefined scope excuses nothing"
);
assert!(shouty.affects_writers());
}
#[test]
fn write_only_is_the_only_scope_a_reader_can_ignore() {
assert!(!ExtensionScope::WriteOnly.affects_readers());
assert!(ExtensionScope::WriteOnly.affects_writers());
assert!(ExtensionScope::ReadWrite.affects_readers());
assert!(ExtensionScope::ReadWrite.affects_writers());
}
}