Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FromWkt trait for reading WKT without exposing the user to the intermediate representation. #95

Merged
merged 2 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
### Added
* impl `std::fmt::Display` for `Wkt`.
* <https://github.com/georust/wkt/pull/88>
* added `wkt_string` and `write_wkt` methods to `ToWkt` trait
* <https://github.com/georust/wkt/pull/89>
* impl `ToWkt` on geo_type Geometry variants directly, so you can `point!(x: 1., y: 2.).wkt_string()`
* <https://github.com/georust/wkt/pull/90>
* `ToWkt` is no longer tied to geo-types. You can implement it on your own
custom (non-geo_type) geometry types.
* <https://github.com/georust/wkt/pull/90>
* Additions to ToWkt trait:
* added `wkt_string` and `write_wkt` methods to `ToWkt` trait
* <https://github.com/georust/wkt/pull/89>
* impl `ToWkt` on geo_type Geometry variants directly, so you can `point!(x: 1., y: 2.).wkt_string()`
* <https://github.com/georust/wkt/pull/90>
* `ToWkt` is no longer tied to geo-types. You can implement it on your own
custom (non-geo_type) geometry types.
* <https://github.com/georust/wkt/pull/90>
* New `FromWkt` trait allows a way to convert from a string or reader directly
to geo-types, without exposing you to the intermediate `Wkt` structs.
* <https://github.com/georust/wkt/pull/95>
* Implemented `geo_types::GeometryCollection::from(Wkt::from_str(wkt_str))`
* <https://github.com/georust/wkt/pull/95>

## 0.10.0 - 2022-02-24
### Changed
Expand Down
5 changes: 3 additions & 2 deletions benches/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extern crate criterion;
extern crate wkt;

use std::str::FromStr;
use wkt::TryFromWkt;

fn bench_parse(c: &mut criterion::Criterion) {
c.bench_function("parse small", |bencher| {
Expand All @@ -24,14 +25,14 @@ fn bench_parse_to_geo(c: &mut criterion::Criterion) {
c.bench_function("parse small to geo", |bencher| {
let s = include_str!("./small.wkt");
bencher.iter(|| {
let _ = geo_types::Geometry::try_from(wkt::Wkt::<f64>::from_str(s).unwrap());
let _ = geo_types::Geometry::<f64>::try_from_wkt_str(s).unwrap();
});
});

c.bench_function("parse big to geo", |bencher| {
let s = include_str!("./big.wkt");
bencher.iter(|| {
let _ = geo_types::Geometry::try_from(wkt::Wkt::<f64>::from_str(s).unwrap());
let _ = geo_types::Geometry::<f64>::try_from_wkt_str(s).unwrap();
});
});
}
Expand Down
31 changes: 31 additions & 0 deletions src/from_wkt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Create geometries from WKT.
///
/// A default implementation exists for [geo-types](../geo-types), or you can implement this trait
/// for your own types.
pub trait TryFromWkt<T>: Sized {
type Error;

/// # Examples
#[cfg_attr(feature = "geo-types", doc = "```")]
#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")]
/// // This example requires the geo-types feature (on by default).
/// use wkt::TryFromWkt;
/// use geo_types::Point;
/// let point: Point<f64> = Point::try_from_wkt_str("POINT(10 20)").unwrap();
/// assert_eq!(point.y(), 20.0);
/// ```
fn try_from_wkt_str(wkt_str: &str) -> Result<Self, Self::Error>;

/// # Examples
#[cfg_attr(feature = "geo-types", doc = "```")]
#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")]
/// // This example requires the geo-types feature (on by default).
/// use wkt::TryFromWkt;
/// use geo_types::Point;
///
/// let fake_file = "POINT(10 20)".as_bytes().to_vec();
/// let point: Point<f64> = Point::try_from_wkt_reader(&*fake_file).unwrap();
/// assert_eq!(point.y(), 20.0);
/// ```
fn try_from_wkt_reader(wkt_reader: impl std::io::Read) -> Result<Self, Self::Error>;
}
153 changes: 151 additions & 2 deletions src/geo_types_from_wkt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
// limitations under the License.

use crate::types::*;
use crate::Geometry;
use crate::Wkt;
use crate::{Geometry, TryFromWkt, Wkt, WktFloat};

use std::any::type_name;
use std::convert::{TryFrom, TryInto};
use std::io::Read;
use std::str::FromStr;

use geo_types::{coord, CoordFloat};
use thiserror::Error;
Expand All @@ -36,6 +38,8 @@ pub enum Error {
},
#[error("Wrong number of Geometries: {0}")]
WrongNumberOfGeometries(usize),
#[error("Invalid WKT")]
InvalidWKT(&'static str),
#[error("External error: {0}")]
External(Box<dyn std::error::Error>),
}
Expand Down Expand Up @@ -85,10 +89,54 @@ try_from_wkt_impl!(
MultiPoint,
MultiLineString,
MultiPolygon,
// See impl below.
// GeometryCollection,
Rect,
Triangle
);

/// Fallibly convert this WKT primitive into this [`geo_types`] primitive
impl<T: CoordFloat> TryFrom<Wkt<T>> for geo_types::GeometryCollection<T> {
type Error = Error;

fn try_from(wkt: Wkt<T>) -> Result<Self, Self::Error> {
match wkt.item {
Geometry::GeometryCollection(collection) => {
let geometries: Result<Vec<geo_types::Geometry<T>>, _> =
collection.0.into_iter().map(TryFrom::try_from).collect();
Ok(geo_types::GeometryCollection(geometries?))
}
// geo_types doesn't implement `Geometry::try_from(geom_collec)` yet
// (see https://github.com/georust/geo/pull/821).
// So instead we synthesize the type of error it *would* return.
Geometry::Point(_) => Err(Error::MismatchedGeometry {
expected: type_name::<Self>(),
found: type_name::<geo_types::Point<T>>(),
}),
Geometry::LineString(_) => Err(Error::MismatchedGeometry {
expected: type_name::<Self>(),
found: type_name::<geo_types::LineString<T>>(),
}),
Geometry::Polygon(_) => Err(Error::MismatchedGeometry {
expected: type_name::<Self>(),
found: type_name::<geo_types::Polygon<T>>(),
}),
Geometry::MultiPoint(_) => Err(Error::MismatchedGeometry {
expected: type_name::<Self>(),
found: type_name::<geo_types::MultiPoint<T>>(),
}),
Geometry::MultiLineString(_) => Err(Error::MismatchedGeometry {
expected: type_name::<Self>(),
found: type_name::<geo_types::MultiLineString<T>>(),
}),
Geometry::MultiPolygon(_) => Err(Error::MismatchedGeometry {
expected: type_name::<Self>(),
found: type_name::<geo_types::MultiPolygon<T>>(),
}),
}
}
}

impl<T> From<Coord<T>> for geo_types::Coordinate<T>
where
T: CoordFloat,
Expand Down Expand Up @@ -305,6 +353,47 @@ where
}
}

/// Macro for implementing TryFromWkt for all the geo-types.
/// Alternatively, we could try to have a kind of blanket implementation on TryFrom<Wkt<T>>,
/// but:
/// 1. what would be the type of TryFromWkt::Error?
/// 2. that would preclude ever having a specialized implementation for geo-types as they'd
/// be ambiguous/redundant.
macro_rules! try_from_wkt_impl {
($($type: ty),*$(,)?) => {
$(
impl<T: WktFloat + FromStr + Default> TryFromWkt<T> for $type {
type Error = Error;
fn try_from_wkt_str(wkt_str: &str) -> Result<Self, Self::Error> {
let wkt = Wkt::from_str(wkt_str).map_err(|e| Error::InvalidWKT(e))?;
Self::try_from(wkt)
}

fn try_from_wkt_reader(mut wkt_reader: impl Read) -> Result<Self, Self::Error> {
let mut bytes = vec![];
wkt_reader.read_to_end(&mut bytes).map_err(|e| Error::External(Box::new(e)))?;
let wkt_str = String::from_utf8(bytes).map_err(|e| Error::External(Box::new(e)))?;
Self::try_from_wkt_str(&wkt_str)
}
}
)*
}
}

try_from_wkt_impl![
geo_types::Geometry<T>,
geo_types::Point<T>,
geo_types::Line<T>,
geo_types::LineString<T>,
geo_types::Polygon<T>,
geo_types::MultiPoint<T>,
geo_types::MultiLineString<T>,
geo_types::MultiPolygon<T>,
geo_types::GeometryCollection<T>,
geo_types::Triangle<T>,
geo_types::Rect<T>,
];

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -876,4 +965,64 @@ mod tests {
w_geometrycollection.try_into().unwrap()
);
}

#[test]
fn geom_collection_from_wkt_str() {
// geometry collections have some special handling vs. other geometries, so we test them separately.
let collection = geo_types::GeometryCollection::<f64>::try_from_wkt_str(
"GeometryCollection(POINT(1 2))",
)
.unwrap();
let point: geo_types::Point<_> = collection[0].clone().try_into().unwrap();
assert_eq!(point.y(), 2.0);
}

#[test]
fn geom_collection_from_invalid_wkt_str() {
// geometry collections have some special handling vs. other geometries, so we test them separately.
let err = geo_types::GeometryCollection::<f64>::try_from_wkt_str("GeomColl(POINT(1 2))")
.unwrap_err();
match err {
Error::InvalidWKT(err_text) => assert_eq!(err_text, "Invalid type encountered"),
e => panic!("Not the error we expected. Found: {}", e),
}
}

#[test]
fn geom_collection_from_other_wkt_str() {
// geometry collections have some special handling vs. other geometries, so we test them separately.
let not_a_collection = geo_types::GeometryCollection::<f64>::try_from_wkt_str("POINT(1 2)");
let err = not_a_collection.unwrap_err();
match err {
Error::MismatchedGeometry {
expected: "geo_types::geometry_collection::GeometryCollection<f64>",
found: "geo_types::point::Point<f64>",
} => {}
e => panic!("Not the error we expected. Found: {}", e),
}
}

#[test]
fn from_invalid_wkt_str() {
let a_point_too_many = geo_types::Point::<f64>::try_from_wkt_str("PINT(1 2)");
let err = a_point_too_many.unwrap_err();
match err {
Error::InvalidWKT(err_text) => assert_eq!(err_text, "Invalid type encountered"),
e => panic!("Not the error we expected. Found: {}", e),
}
}

#[test]
fn from_other_geom_wkt_str() {
let not_actually_a_line_string =
geo_types::LineString::<f64>::try_from_wkt_str("POINT(1 2)");
let err = not_actually_a_line_string.unwrap_err();
match err {
Error::MismatchedGeometry {
expected: "geo_types::line_string::LineString<f64>",
found: "geo_types::point::Point<f64>",
} => {}
e => panic!("Not the error we expected. Found: {}", e),
}
}
}
60 changes: 39 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//! The `wkt` crate provides conversions to and from [`WKT`](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) primitive types.
//! See the [`types`](crate::types) module for a list of available types.
//! The `wkt` crate provides conversions to and from the [WKT (Well Known Text)](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry)
//! geometry format.
//!
//! Conversions (using [`std::convert::From`] and [`std::convert::TryFrom`]) to and from [`geo_types`] primitives are enabled by default, but the feature is **optional**.
//! Conversions are available via the [`TryFromWkt`] and [`ToWkt`] traits, with implementations for
//! [`geo_types`] primitives enabled by default.
//!
//! Enable the `serde` feature if you need to deserialise data into custom structs containing `WKT` geometry fields.
//! For advanced usage, see the [`types`](crate::types) module for a list of internally used types.
//!
//! Enable the `serde` feature if you need to deserialise data into custom structs containing `WKT`
//! geometry fields.
//!
//! # Examples
//!
//! ## Read `geo_types` from a WKT string
#![cfg_attr(feature = "geo-types", doc = "```")]
#![cfg_attr(not(feature = "geo-types"), doc = "```ignore")]
//! // This example requires the geo-types feature (on by default).
//! use wkt::TryFromWkt;
//! use geo_types::Point;
//!
//! let point: Point<f64> = Point::try_from_wkt_str("POINT(10 20)").unwrap();
//! assert_eq!(point.y(), 20.0);
//! ```
//! use std::str::FromStr;
//! use wkt::Wkt;
//! let point: Wkt<f64> = Wkt::from_str("POINT(10 20)").unwrap();
//! ```
//!
//! ## Write `geo_types` to a WKT string
#![cfg_attr(feature = "geo-types", doc = "```")]
#![cfg_attr(not(feature = "geo-types"), doc = "```ignore")]
//! // Convert to a geo_types primitive from a Wkt struct
//! // This example requires the geo-types feature (on by default).
//! use std::convert::TryInto;
//! use std::str::FromStr;
//! use wkt::Wkt;
//! use wkt::ToWkt;
//! use geo_types::Point;
//!
//! let point: Wkt<f64> = Wkt::from_str("POINT(10 20)").unwrap();
//! let g_point: geo_types::Point<f64> = (10., 20.).into();
//! // We can attempt to directly convert the Wkt without having to access its items field
//! let converted: geo_types::Point<f64> = point.try_into().unwrap();
//! assert_eq!(g_point, converted);
//! let point: Point<f64> = Point::new(1.0, 2.0);
//! assert_eq!(point.wkt_string(), "POINT(1 2)");
//! ```
//!
//! ## Direct Access to the `item` Field
//! If you wish to work directly with one of the WKT [`types`] you can match on the `item` field
//! ## Read or write your own geometry types
//!
//! Not using `geo-types` for your geometries? No problem!
//!
//! You can use [`Wkt::from_str`] to parse a WKT string into this crate's intermediate geometry
//! structure. You can use that directly, or if have your own geometry types that you'd prefer to
//! use, utilize that [`Wkt`] struct to implement the [`ToWkt`] or [`TryFromWkt`] traits for your
//! own types.
//!
//! In doing so, you'll likely want to match on one of the WKT [`types`] (Point, Linestring, etc.)
//! stored in its `item` field
//! ```
//! use std::convert::TryInto;
//! use std::str::FromStr;
//! use wkt::Wkt;
//! use wkt::Geometry;
//!
//! let wktls: Wkt<f64> = Wkt::from_str("LINESTRING(10 20, 20 30)").unwrap();
//! let ls = match wktls.item {
//! Geometry::LineString(line_string) => {
//! // you now have access to the types::LineString
//! // you now have access to the `wkt::types::LineString`.
//! assert_eq!(line_string.0[0].x, 10.0);
//! }
//! _ => unreachable!(),
//! };
Expand Down Expand Up @@ -95,6 +110,9 @@ mod geo_types_to_wkt;
extern crate serde;
#[cfg(feature = "serde")]
pub mod deserialize;
mod from_wkt;
pub use from_wkt::TryFromWkt;

#[cfg(all(feature = "serde", feature = "geo-types"))]
pub use deserialize::{deserialize_geometry, deserialize_point};

Expand Down