1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! A value is the part of an Atom that contains the actual information.

use crate::{datatype::DataType, datatype::match_datatype, errors::AtomicResult, resources::PropVals};
use regex::Regex;
use serde::{Deserialize, Serialize};

/// An individual Value in an Atom, represented as a native Rust enum.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Value {
    AtomicUrl(String),
    Date(String),
    Integer(isize),
    Markdown(String),
    ResourceArray(Vec<String>),
    Slug(String),
    String(String),
    /// Unix Epoch datetime in milliseconds
    Timestamp(i64),
    NestedResource(PropVals),
    Boolean(bool),
    Unsupported(UnsupportedValue),
}

/// When the Datatype of a Value is not handled by this library
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UnsupportedValue {
    pub value: String,
    /// URL of the datatype
    pub datatype: String,
}

/// Only alphanumeric characters, no spaces
pub const SLUG_REGEX: &str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$";
/// YYYY-MM-DD
pub const DATE_REGEX: &str = r"^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$";

impl Value {

    /// Returns the datatype for the value
    pub fn datatype(&self) -> DataType {
        match self {
            Value::AtomicUrl(_) => DataType::AtomicUrl,
            Value::Date(_) => DataType::Date,
            Value::Integer(_) => DataType::Integer,
            Value::Markdown(_) => DataType::Markdown,
            Value::ResourceArray(_) => DataType::ResourceArray,
            Value::Slug(_) => DataType::Slug,
            Value::String(_) => DataType::String,
            Value::Timestamp(_) => DataType::Timestamp,
            // TODO: these datatypes are not the same
            Value::NestedResource(_) => DataType::AtomicUrl,
            Value::Boolean(_) => DataType::Boolean,
            Value::Unsupported(s) => DataType::Unsupported(s.datatype.clone())
        }
    }

    /// Creates a new Value from an explicit DataType.
    /// Fails if the input string does not convert.
    pub fn new(value: &str, datatype: &DataType) -> AtomicResult<Value> {
        match datatype {
            DataType::Integer => {
                let val: isize = value.parse()?;
                Ok(Value::Integer(val))
            }
            DataType::String => Ok(Value::String(value.into())),
            DataType::Markdown => Ok(Value::Markdown(value.into())),
            DataType::Slug => {
                let re = Regex::new(SLUG_REGEX).unwrap();
                if re.is_match(&*value) {
                    return Ok(Value::Slug(value.into()));
                }
                Err(format!("Not a valid slug: {}. Only alphanumerics, no spaces allowed.", value).into())
            }
            DataType::AtomicUrl => Ok(Value::AtomicUrl(value.into())),
            DataType::ResourceArray => {
                let vector: Vec<String> = crate::parse::parse_json_array(&value).map_err(|e| {
                    return format!("Could not deserialize ResourceArray: {}. Should be a JSON array of strings. {}", &value, e);
                })?;
                Ok(Value::ResourceArray(vector))
            }
            DataType::Date => {
                let re = Regex::new(DATE_REGEX).unwrap();
                if re.is_match(&*value) {
                    return Ok(Value::Date(value.into()));
                }
                Err(format!("Not a valid date: {}. Needs to be YYYY-MM-DD.", value).into())
            }
            DataType::Timestamp => {
                let val: i64 = value
                    .parse()
                    .map_err(|e| return format!("Not a valid Timestamp: {}. {}", value, e))?;
                Ok(Value::Timestamp(val))
            }
            DataType::Unsupported(unsup_url) => Ok(Value::Unsupported(UnsupportedValue {
                value: value.into(),
                datatype: unsup_url.into(),
            })),
            DataType::Boolean => {
                let bool = match value {
                    "true" => true,
                    "false" => false,
                    other => return Err(format!("Not a valid boolean value: {}, should be 'true' or 'false'.", other).into()),
                };
                Ok(Value::Boolean(bool))
            }
        }
    }

    /// Returns a new Value, accepts a datatype string
    pub fn new_from_string(value: &str, datatype: &str) -> AtomicResult<Value> {
        Value::new(value, &match_datatype(datatype))
    }

    /// Returns a Vector, if the Atom is one
    pub fn to_vec(&self) -> AtomicResult<&Vec<String>> {
        if let Value::ResourceArray(arr) = self {
            return Ok(arr)
        }
        Err(format!("Value {} is not a Resource Array", self).into())
    }
}

impl From<String> for Value {
    fn from(val: String) -> Self {
        Value::String(val)
    }
}

impl From<i32> for Value {
    fn from(val: i32) -> Self {
        Value::Integer(val as isize)
    }
}

impl From<u64> for Value {
    fn from(val: u64) -> Self {
        Value::Integer(val as isize)
    }
}

impl From<usize> for Value {
    fn from(val: usize) -> Self {
        Value::Integer(val as isize)
    }
}

impl From<Vec<String>> for Value {
    fn from(val: Vec<String>) -> Self {
        Value::ResourceArray(val)
    }
}

impl From<PropVals> for Value {
    fn from(val: PropVals) -> Self {
        Value::NestedResource(val)
    }
}

impl From<bool> for Value {
    fn from(val: bool) -> Self {
        Value::Boolean(val)
    }
}

use std::fmt;
impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Value::AtomicUrl(s) => write!(f, "{}", s),
            Value::Date(s) => write!(f, "{}", s),
            Value::Integer(i) => write!(f, "{}", i),
            Value::Markdown(i) => write!(f, "{}", i),
            Value::ResourceArray(v) => {
                let s = crate::serialize::serialize_json_array_owned(v)
                    .unwrap_or_else(|_e| format!("[Could not serialize resource array: {:?}", v));
                write!(f, "{}", s)
            }
            Value::Slug(s) => write!(f, "{}", s),
            Value::String(s) => write!(f, "{}", s),
            Value::Timestamp(i) => write!(f, "{}", i),
            Value::NestedResource(n) => write!(f, "{:?}", n),
            Value::Boolean(b) => write!(f, "{}", b),
            Value::Unsupported(u) => write!(f, "{}", u.value),
        }
    }
}

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

    #[test]
    fn formats_correct_value() {
        let int = Value::new("8", &DataType::Integer).unwrap();
        assert!(int.to_string() == "8");
        let string = Value::new("string", &DataType::String).unwrap();
        assert!(string.to_string() == "string");
        let date = Value::new("1200-02-02", &DataType::Date).unwrap();
        assert!(date.to_string() == "1200-02-02");

        let converted  = Value::from(8);
        assert!(converted.to_string() == "8");
    }

    #[test]
    fn fails_wrong_values() {
        Value::new("no int", &DataType::Integer).unwrap_err();
        Value::new("no spaces", &DataType::Slug).unwrap_err();
        Value::new("120-02-02", &DataType::Date).unwrap_err();
        Value::new("12000-02-02", &DataType::Date).unwrap_err();
    }
}