use crate::document::{DocumentError, DocumentResult, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
String,
Number,
Bool,
Null,
Json,
}
impl ValueType {
pub fn parse(name: &str) -> Option<Self> {
match name {
"string" => Some(Self::String),
"number" => Some(Self::Number),
"bool" => Some(Self::Bool),
"null" => Some(Self::Null),
"json" => Some(Self::Json),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Self::String => "string",
Self::Number => "number",
Self::Bool => "bool",
Self::Null => "null",
Self::Json => "json",
}
}
}
pub fn value_from_type(value_type: ValueType, raw: Option<&str>) -> DocumentResult<Value> {
match value_type {
ValueType::Null => match raw {
None => Ok(Value::Null),
Some(_) => Err(DocumentError::ParseError {
format: "value".to_string(),
detail: "--value-type null takes no VALUE".to_string(),
}),
},
ValueType::String => Ok(Value::String(require_value(raw, value_type)?.to_string())),
ValueType::Bool => {
let raw = require_value(raw, value_type)?;
parse_bool(raw).map(Value::Bool).ok_or_else(|| {
DocumentError::ParseError {
format: "boolean".to_string(),
detail: format!(
"invalid --value-type bool value `{raw}`; expected true/false, yes/no, on/off, or 1/0"
),
}
})
}
ValueType::Number => parse_number_literal(require_value(raw, value_type)?),
ValueType::Json => {
let raw = require_value(raw, value_type)?;
serde_json::from_str::<serde_json::Value>(raw)
.map(Value::from)
.map_err(|error| DocumentError::ParseError {
format: "JSON".to_string(),
detail: error.to_string(),
})
}
}
}
fn require_value(raw: Option<&str>, value_type: ValueType) -> DocumentResult<&str> {
raw.ok_or_else(|| DocumentError::ParseError {
format: "value".to_string(),
detail: format!("--value-type {} requires a VALUE", value_type.name()),
})
}
fn parse_number_literal(text: &str) -> DocumentResult<Value> {
serde_json::from_str::<serde_json::Value>(text)
.ok()
.filter(serde_json::Value::is_number)
.map(Value::from)
.ok_or_else(|| DocumentError::ParseError {
format: "number".to_string(),
detail: format!("invalid --value-type number literal `{text}`"),
})
}
fn parse_bool(value: &str) -> Option<bool> {
match value.to_ascii_lowercase().as_str() {
"true" | "yes" | "on" | "1" => Some(true),
"false" | "no" | "off" | "0" => Some(false),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarKind {
Null,
Bool,
Number,
String,
}
impl ScalarKind {
pub fn value_type_name(self) -> &'static str {
match self {
Self::Null => "null",
Self::Bool => "bool",
Self::Number => "number",
Self::String => "string",
}
}
}
pub fn scalar_kind(value: &Value) -> Option<ScalarKind> {
match value {
Value::Null => Some(ScalarKind::Null),
Value::Bool(_) => Some(ScalarKind::Bool),
Value::Integer(_) | Value::Unsigned(_) | Value::Float(_) | Value::Number(_) => {
Some(ScalarKind::Number)
}
Value::String(_) => Some(ScalarKind::String),
Value::Array(_) | Value::Object(_) => None,
}
}
pub fn guard_bare_overwrite(existing: Option<&Value>) -> Result<(), ScalarKind> {
match existing.and_then(scalar_kind) {
Some(ScalarKind::String) | None => Ok(()),
Some(other) => Err(other),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
#[test]
fn bare_string_is_zero_coercion() {
for raw in ["007", "1.0", "true", "null", "3e10"] {
assert_eq!(
value_from_type(ValueType::String, Some(raw)).unwrap(),
Value::String(raw.to_string())
);
}
}
#[test]
fn value_type_number_is_literal_faithful() {
assert_eq!(
value_from_type(ValueType::Number, Some("18446744073709551615")).unwrap(),
Value::Unsigned(u64::MAX)
);
let huge = "123456789012345678901234567890";
assert_eq!(
value_from_type(ValueType::Number, Some(huge)).unwrap(),
Value::Number(huge.to_string())
);
let precise = "0.1000000000000000055511151231257827";
assert_eq!(
value_from_type(ValueType::Number, Some(precise)).unwrap(),
Value::Number(precise.to_string())
);
}
#[test]
fn value_type_number_rejects_leading_zero_and_non_numeric() {
assert!(value_from_type(ValueType::Number, Some("007")).is_err());
assert!(value_from_type(ValueType::Number, Some("abc")).is_err());
assert!(value_from_type(ValueType::Number, Some("+5")).is_err());
}
#[test]
fn value_type_bool_is_lenient() {
assert_eq!(
value_from_type(ValueType::Bool, Some("yes")).unwrap(),
Value::Bool(true)
);
assert!(value_from_type(ValueType::Bool, Some("nope")).is_err());
}
#[test]
fn value_type_null_takes_no_value() {
assert_eq!(value_from_type(ValueType::Null, None).unwrap(), Value::Null);
assert!(value_from_type(ValueType::String, None).is_err());
assert!(value_from_type(ValueType::Null, Some("x")).is_err());
}
#[test]
fn value_type_json_is_the_only_container_entry_point() {
let value = value_from_type(ValueType::Json, Some(r#"["a","b"]"#)).unwrap();
assert_eq!(
value,
Value::Array(vec![
Value::String("a".to_string()),
Value::String("b".to_string())
])
);
assert_eq!(
value_from_type(ValueType::Json, Some("\"8080\"")).unwrap(),
Value::String("8080".to_string())
);
}
#[test]
fn guard_fires_only_for_bare_overwrite_of_a_differently_kinded_scalar() {
assert_eq!(guard_bare_overwrite(None), Ok(()));
assert_eq!(
guard_bare_overwrite(Some(&Value::String("x".to_string()))),
Ok(())
);
assert_eq!(guard_bare_overwrite(Some(&Value::Array(vec![]))), Ok(()));
assert_eq!(
guard_bare_overwrite(Some(&Value::Integer(8080))),
Err(ScalarKind::Number)
);
assert_eq!(
guard_bare_overwrite(Some(&Value::Bool(true))),
Err(ScalarKind::Bool)
);
assert_eq!(
guard_bare_overwrite(Some(&Value::Null)),
Err(ScalarKind::Null)
);
}
}