use crate::{Coord, Number, Ref};
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
use serde_json::Value;
pub trait ValueExt {
fn as_hs_coord(&self) -> Option<Coord>;
fn as_hs_date(&self) -> Option<NaiveDate>;
fn as_hs_date_time(&self) -> Option<(DateTime<FixedOffset>, &str)>;
fn as_hs_number(&self) -> Option<Number>;
fn as_hs_ref(&self) -> Option<Ref>;
fn as_hs_str(&self) -> Option<&str>;
fn as_hs_time(&self) -> Option<NaiveTime>;
fn as_hs_uri(&self) -> Option<&str>;
fn as_hs_xstr(&self) -> Option<&str>;
fn is_hs_coord(&self) -> bool;
fn is_hs_date(&self) -> bool;
fn is_hs_date_time(&self) -> bool;
fn is_hs_marker(&self) -> bool;
fn is_hs_na(&self) -> bool;
fn is_hs_number(&self) -> bool;
fn is_hs_ref(&self) -> bool;
fn is_hs_remove_marker(&self) -> bool;
fn is_hs_str(&self) -> bool;
fn is_hs_time(&self) -> bool;
fn is_hs_uri(&self) -> bool;
fn is_hs_xstr(&self) -> bool;
}
impl ValueExt for Value {
fn as_hs_coord(&self) -> Option<Coord> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::Coord => {
let mut split = trim_hs_prefix(s).split(',');
let lat = split.next().and_then(|s| str::parse(s).ok());
let lng = split.next().and_then(|s| str::parse(s).ok());
match (lat, lng) {
(Some(lat), Some(lng)) => Some(Coord::new(lat, lng)),
_ => None,
}
}
_ => None,
})
}
fn as_hs_date(&self) -> Option<NaiveDate> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::Date => {
let date_str = trim_hs_prefix(s);
NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok()
}
_ => None,
})
}
fn as_hs_date_time(&self) -> Option<(DateTime<FixedOffset>, &str)> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::DateTime => {
let mut split = trim_hs_prefix(s).split(' ');
let date_time = split
.next()
.and_then(|s| DateTime::parse_from_rfc3339(s).ok());
let time_zone_name = split.next();
let tuple = (date_time, time_zone_name);
match tuple {
(Some(date_time), Some(time_zone_name)) => {
Some((date_time, time_zone_name))
}
_ => None,
}
}
_ => None,
})
}
fn as_hs_number(&self) -> Option<Number> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::Number => {
Number::from_encoded_json_string(s).ok()
}
_ => None,
})
}
fn as_hs_ref(&self) -> Option<Ref> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::Ref => {
Ref::from_encoded_json_string(s).ok()
}
_ => None,
})
}
fn as_hs_str(&self) -> Option<&str> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::PlainString => Some(s),
JsonStringHaystackType::PrefixedString => Some(trim_hs_prefix(s)),
_ => None,
})
}
fn as_hs_time(&self) -> Option<NaiveTime> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::Time => {
let time_str = trim_hs_prefix(s);
NaiveTime::parse_from_str(time_str, "%k:%M:%S")
.ok()
.or_else(|| {
NaiveTime::parse_from_str(time_str, "%k:%M").ok()
})
}
_ => None,
})
}
fn as_hs_uri(&self) -> Option<&str> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::Uri => Some(trim_hs_prefix(s)),
_ => None,
})
}
fn as_hs_xstr(&self) -> Option<&str> {
self.as_str().and_then(|s| match haystack_type(s) {
JsonStringHaystackType::XStr => Some(trim_hs_prefix(s)),
_ => None,
})
}
fn is_hs_coord(&self) -> bool {
self.as_hs_coord().is_some()
}
fn is_hs_date(&self) -> bool {
self.as_hs_date().is_some()
}
fn is_hs_date_time(&self) -> bool {
self.as_hs_date_time().is_some()
}
fn is_hs_marker(&self) -> bool {
if let Some(s) = self.as_str() {
match haystack_type(s) {
JsonStringHaystackType::Marker => true,
_ => false,
}
} else {
false
}
}
fn is_hs_na(&self) -> bool {
if let Some(s) = self.as_str() {
match haystack_type(s) {
JsonStringHaystackType::Na => true,
_ => false,
}
} else {
false
}
}
fn is_hs_number(&self) -> bool {
self.as_hs_number().is_some()
}
fn is_hs_ref(&self) -> bool {
self.as_hs_ref().is_some()
}
fn is_hs_remove_marker(&self) -> bool {
if let Some(s) = self.as_str() {
match haystack_type(s) {
JsonStringHaystackType::RemoveMarker => true,
_ => false,
}
} else {
false
}
}
fn is_hs_str(&self) -> bool {
self.as_hs_str().is_some()
}
fn is_hs_time(&self) -> bool {
self.as_hs_time().is_some()
}
fn is_hs_uri(&self) -> bool {
self.as_hs_uri().is_some()
}
fn is_hs_xstr(&self) -> bool {
self.as_hs_xstr().is_some()
}
}
fn haystack_type(s: &str) -> JsonStringHaystackType {
if let Some(prefix) = first_two_chars(s) {
match prefix.as_ref() {
"m:" => JsonStringHaystackType::Marker,
"-:" => JsonStringHaystackType::RemoveMarker,
"z:" => JsonStringHaystackType::Na,
"n:" => JsonStringHaystackType::Number,
"r:" => JsonStringHaystackType::Ref,
"s:" => JsonStringHaystackType::PrefixedString,
"d:" => JsonStringHaystackType::Date,
"h:" => JsonStringHaystackType::Time,
"t:" => JsonStringHaystackType::DateTime,
"u:" => JsonStringHaystackType::Uri,
"c:" => JsonStringHaystackType::Coord,
"x:" => JsonStringHaystackType::XStr,
_ => JsonStringHaystackType::PlainString,
}
} else {
JsonStringHaystackType::PlainString
}
}
fn first_two_chars(s: &str) -> Option<String> {
let prefix: String = s.chars().take(2).collect();
if prefix.chars().count() == 2 {
Some(prefix)
} else {
None
}
}
fn trim_hs_prefix(s: &str) -> &str {
&s[2..]
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum JsonStringHaystackType {
Marker,
RemoveMarker,
Na,
Number,
Ref,
PlainString,
PrefixedString,
Date,
Time,
DateTime,
Uri,
Coord,
XStr,
}
#[cfg(test)]
mod test {
use super::ValueExt;
use serde_json::json;
#[test]
fn haystack_type_strings() {
use super::{haystack_type, JsonStringHaystackType};
assert_eq!(haystack_type(""), JsonStringHaystackType::PlainString);
assert_eq!(haystack_type(":"), JsonStringHaystackType::PlainString);
assert_eq!(haystack_type("5"), JsonStringHaystackType::PlainString);
assert_eq!(haystack_type("w:"), JsonStringHaystackType::PlainString);
assert_eq!(haystack_type("hello"), JsonStringHaystackType::PlainString);
assert_eq!(haystack_type("s:"), JsonStringHaystackType::PrefixedString);
assert_eq!(
haystack_type("s:hello"),
JsonStringHaystackType::PrefixedString
);
assert_eq!(
haystack_type("s:hello world"),
JsonStringHaystackType::PrefixedString
);
}
#[test]
fn haystack_type_non_strings() {
use super::{haystack_type, JsonStringHaystackType};
assert_eq!(haystack_type("m:"), JsonStringHaystackType::Marker);
assert_eq!(haystack_type("m:junk"), JsonStringHaystackType::Marker);
assert_eq!(haystack_type("-:"), JsonStringHaystackType::RemoveMarker);
assert_eq!(
haystack_type("-:junk"),
JsonStringHaystackType::RemoveMarker
);
assert_eq!(haystack_type("z:"), JsonStringHaystackType::Na);
assert_eq!(haystack_type("z: junk"), JsonStringHaystackType::Na);
assert_eq!(
haystack_type("n:55 celsius"),
JsonStringHaystackType::Number
);
assert_eq!(
haystack_type("r:p:proj:r:abcd1234-abcd1234"),
JsonStringHaystackType::Ref
);
assert_eq!(haystack_type("d:2014-01-03"), JsonStringHaystackType::Date);
assert_eq!(haystack_type("h:23:59"), JsonStringHaystackType::Time);
assert_eq!(
haystack_type("t:2015-06-08T15:47:41-04:00 New_York"),
JsonStringHaystackType::DateTime
);
assert_eq!(
haystack_type("u:http://project-haystack.org/"),
JsonStringHaystackType::Uri
);
assert_eq!(
haystack_type("c:37.545,-77.449"),
JsonStringHaystackType::Coord
);
assert_eq!(haystack_type("x:Type:value"), JsonStringHaystackType::XStr);
}
#[test]
fn as_hs_str() {
let plain_val = json!("hello world");
let prefixed_val = json!("s:hello world");
assert_eq!(plain_val.as_hs_str(), Some("hello world"));
assert_eq!(prefixed_val.as_hs_str(), Some("hello world"));
}
#[test]
fn as_hs_ref() {
let ref_val = json!("r:abc-123");
let ref_val_and_display_name = json!("r:abc-123 RTU #3");
assert_eq!(ref_val.as_hs_ref().unwrap().as_ref(), "@abc-123");
assert_eq!(
ref_val_and_display_name.as_hs_ref().unwrap().as_ref(),
"@abc-123"
);
}
#[test]
fn as_hs_number() {
use crate::Number;
let number_val = json!("n:25.123 celsius");
assert_eq!(
number_val.as_hs_number().unwrap(),
Number::new(25.123, Some("celsius".to_owned()))
);
let number_no_unit_val = json!("n:25.123");
assert_eq!(
number_no_unit_val.as_hs_number().unwrap(),
Number::new(25.123, None)
);
}
#[test]
fn as_hs_date() {
use chrono::NaiveDate;
let number_val = json!("d:2014-12-01");
assert_eq!(
number_val.as_hs_date().unwrap(),
NaiveDate::from_ymd(2014, 12, 1)
);
}
#[test]
fn as_hs_time() {
use chrono::NaiveTime;
let time_val = json!("h:23:59");
let time = time_val.as_hs_time().unwrap();
assert_eq!(time, NaiveTime::from_hms(23, 59, 0));
}
#[test]
fn as_hs_time_with_seconds() {
use chrono::NaiveTime;
let time_val = json!("h:23:59:15");
let time = time_val.as_hs_time().unwrap();
assert_eq!(time, NaiveTime::from_hms(23, 59, 15));
}
#[test]
fn as_hs_time_with_no_hour_padding() {
use chrono::NaiveTime;
let time_val = json!("h:3:59");
let time = time_val.as_hs_time().unwrap();
assert_eq!(time, NaiveTime::from_hms(3, 59, 0));
}
#[test]
fn as_hs_time_with_no_hour_padding_and_seconds() {
use chrono::NaiveTime;
let time_val = json!("h:3:59:15");
let time = time_val.as_hs_time().unwrap();
assert_eq!(time, NaiveTime::from_hms(3, 59, 15));
}
#[test]
fn as_hs_uri() {
let uri_val = json!("u:www.test.com");
let uri = uri_val.as_hs_uri().unwrap();
assert_eq!(uri, "www.test.com");
}
#[test]
fn as_hs_xstr() {
let xstr_val = json!("x:Type:value");
let xstr = xstr_val.as_hs_xstr().unwrap();
assert_eq!(xstr, "Type:value");
}
#[test]
fn as_hs_coord() {
use crate::Coord;
let coord_val = json!("c:37.545,-77.449");
let coord = coord_val.as_hs_coord().unwrap();
assert_eq!(coord, Coord::new(37.545, -77.449));
}
#[test]
fn as_hs_date_time() {
use chrono::{FixedOffset, TimeZone};
let hour = 3600;
let dt_val = json!("t:2015-06-08T15:47:41-04:00 New_York");
let (dt, tz_name) = dt_val.as_hs_date_time().unwrap();
assert_eq!(tz_name, "New_York");
assert_eq!(
dt,
FixedOffset::west(4 * hour)
.ymd(2015, 6, 8)
.and_hms(15, 47, 41),
);
}
}