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

Use tilejson! macro for instantiation, refactor #16

Merged
merged 3 commits into from
May 25, 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
7 changes: 7 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
- name: Run cargo docs
uses: actions-rs/cargo@v1
env:
RUSTDOCFLAGS: -D warnings
with:
command: doc
args: --no-deps
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
* update docs to match v3.0.0 spec
* add `fillzoom` field per v3.0.0 spec
* add `Center` and `Bounds` structs instead of arrays
* both support `FromStr` trait
* add `VectorLayer` struct and the `vector_layer` field
* Remove builder pattern because `TileJSON` is writable
* Add `other` fields for any unknown fields in root and vector layers
* Restructure instantiation:
* use `new(source)` or `new_ext(sources, version)` to create `TileJSON`
* use `tilejson!{ source }` macro to create `TileJSON` objects, with any number of the optional `field: value` pairs.
* use `set_missing_defaults()` to replace all missing values with their defaults (only if the spec defines it)
* Remove `id` field because it is not supported by the spec

Expand Down
128 changes: 128 additions & 0 deletions src/bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
use std::fmt::{Display, Formatter};
use std::num::ParseFloatError;
use std::str::FromStr;

#[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Debug, Copy, Clone)]
pub struct Bounds {
pub left: f64,
pub bottom: f64,
pub right: f64,
pub top: f64,
}

impl Bounds {
pub fn new(left: f64, bottom: f64, right: f64, top: f64) -> Self {
Self {
left,
bottom,
right,
top,
}
}
}

impl Default for Bounds {
/// Default bounds are set to `[-180, -85.05112877980659, 180, 85.0511287798066]`
/// See <https://github.com/mapbox/tilejson-spec/tree/master/3.0.0#35-bounds>
fn default() -> Self {
Self::new(-180.0, -85.05112877980659, 180.0, 85.0511287798066)
}
}

#[derive(Debug, PartialEq, Clone)]
pub enum ParseBoundsError {
/// Incorrect number of values
BadLen,
/// Wrapped error from the parse::<f64>()
ParseCoordError(ParseFloatError),
}

impl Display for ParseBoundsError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseBoundsError::BadLen => {
f.write_str("Incorrect number of values. Bounds expects four f64 values.")
}
ParseBoundsError::ParseCoordError(e) => e.fmt(f),
}
}
}

impl TryFrom<Vec<f64>> for Bounds {
type Error = ParseBoundsError;

/// Parse four f64 values as a Bounds value, same order as the [Bounds::new] constructor.
fn try_from(value: Vec<f64>) -> Result<Self, Self::Error> {
if value.len() == 4 {
Ok(Self {
left: value[0],
bottom: value[1],
right: value[2],
top: value[3],
})
} else {
Err(ParseBoundsError::BadLen)
}
}
}

impl FromStr for Bounds {
type Err = ParseBoundsError;

/// Parse a string of four comma-separated values as a Bounds value,
/// same order as the [Bounds::new] constructor. Extra spaces are ignored.
///
/// # Example
/// ```
/// # use tilejson::Bounds;
/// # use std::str::FromStr;
/// let bounds = Bounds::from_str("-1.0, -2.0, 3, 4").unwrap();
/// assert_eq!(bounds, Bounds::new(-1.0, -2.0, 3.0, 4.0));
/// ```
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut vals = s.split(',').map(|s| s.trim());
let mut next_val = || {
vals.next().map_or(Err(ParseBoundsError::BadLen), |v| {
v.parse().map_err(ParseBoundsError::ParseCoordError)
})
};
let bounds = Self {
left: next_val()?,
bottom: next_val()?,
right: next_val()?,
top: next_val()?,
};
match vals.next() {
Some(_) => Err(ParseBoundsError::BadLen),
None => Ok(bounds),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_err() {
const E_EMPTY: &str = "cannot parse float from empty string";
const E_FORMAT: &str = "invalid float literal";
const E_LEN: &str = "Incorrect number of values. Bounds expects four f64 values.";

let err_to_str = |v| Bounds::from_str(v).unwrap_err().to_string();

assert_eq!(err_to_str(""), E_EMPTY);
assert_eq!(err_to_str("1"), E_LEN);
assert_eq!(err_to_str("1,2,3"), E_LEN);
assert_eq!(err_to_str("1,2,3,4,5"), E_LEN);
assert_eq!(err_to_str("1,2,3,a"), E_FORMAT);
}

#[test]
fn test_parse() {
let val = |s| Bounds::from_str(s).unwrap();
assert_eq!(val("0,0,0,0"), Bounds::new(0.0, 0.0, 0.0, 0.0));
assert_eq!(val(" 1 ,2.0, 3.0, 4.0 "), Bounds::new(1.0, 2.0, 3.0, 4.0));
}
}
105 changes: 105 additions & 0 deletions src/center.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
use std::fmt::{Display, Formatter};
use std::num::{ParseFloatError, ParseIntError};
use std::str::FromStr;

#[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Debug, Default, Copy, Clone)]
pub struct Center {
pub longitude: f64,
pub latitude: f64,
pub zoom: u8,
}

impl Center {
pub fn new(longitude: f64, latitude: f64, zoom: u8) -> Self {
Self {
longitude,
latitude,
zoom,
}
}
}

#[derive(Debug, PartialEq, Clone)]
pub enum ParseCenterError {
/// Incorrect number of values
BadLen,
/// Wrapped error from the parse::<f64>()
ParseCoordError(ParseFloatError),
/// Wrapped error from the parse::<u8>()
ParseZoomError(ParseIntError),
}

impl Display for ParseCenterError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseCenterError::BadLen => {
f.write_str("Incorrect number of values. Center expects two f64 and one u8 values.")
}
ParseCenterError::ParseCoordError(e) => e.fmt(f),
ParseCenterError::ParseZoomError(e) => e.fmt(f),
}
}
}

impl FromStr for Center {
type Err = ParseCenterError;

/// Parse a string of four comma-separated values as a Center value,
/// same order as the [Center::new] constructor. Extra spaces are ignored.
///
/// # Example
/// ```
/// # use tilejson::Center;
/// # use std::str::FromStr;
/// let center = Center::from_str("1.0, 2.0, 3").unwrap();
/// assert_eq!(center, Center::new(1.0, 2.0, 3));
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut vals = s.split(',').map(|s| s.trim());
let mut next_val = || vals.next().ok_or(ParseCenterError::BadLen);
let center = Self {
longitude: next_val()?
.parse()
.map_err(ParseCenterError::ParseCoordError)?,
latitude: next_val()?
.parse()
.map_err(ParseCenterError::ParseCoordError)?,
zoom: next_val()?
.parse()
.map_err(ParseCenterError::ParseZoomError)?,
};
match vals.next() {
Some(_) => Err(ParseCenterError::BadLen),
None => Ok(center),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_err() {
const E_EMPTY: &str = "cannot parse float from empty string";
const E_FORMAT: &str = "invalid digit found in string";
const E_LEN: &str = "Incorrect number of values. Center expects two f64 and one u8 values.";

let err_to_str = |s| Center::from_str(s).unwrap_err().to_string();

assert_eq!(err_to_str(""), E_EMPTY);
assert_eq!(err_to_str("1"), E_LEN);
assert_eq!(err_to_str("1,2"), E_LEN);
assert_eq!(err_to_str("1,2,3,4"), E_LEN);
assert_eq!(err_to_str("1,2,a"), E_FORMAT);
assert_eq!(err_to_str("1,2,1.1"), E_FORMAT);
assert_eq!(err_to_str("1,,0"), E_EMPTY);
}

#[test]
fn test_parse() {
let val = |s| Center::from_str(s).unwrap();
assert_eq!(val("0,0,0"), Center::new(0.0, 0.0, 0));
assert_eq!(val(" 1 ,2.0, 3 "), Center::new(1.0, 2.0, 3));
}
}
15 changes: 13 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
//! # TileJSON
//!
//! `tilejson` is a crate for serializing/deserializing TileJSON format —
//! `tilejson` is a crate for serializing/deserializing
//! [TileJSON format](https://github.com/mapbox/tilejson-spec) —
//! an open standard for representing map metadata.
//!
//! Use [tilejson!] macro to instantiate a valid [TileJSON].
//! Use [TileJSON::set_missing_defaults] to populate default values per spec.

mod bounds;
mod center;
mod tilejson;
mod vector_layer;

pub mod tilejson;
pub use crate::bounds::*;
pub use crate::center::*;
pub use crate::tilejson::*;
pub use crate::vector_layer::*;
Loading