use serde_json::{Map, Number, Value};
use crate::json::{py_str, repr};
use crate::{Error, Result};
pub fn to_str(value: &Value) -> String {
py_str(value)
}
pub fn to_int(value: &Value) -> Result<i64> {
match value {
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(i)
} else if let Some(u) = n.as_u64() {
i64::try_from(u).map_err(|_| Error::Value(format!("integer {u} is out of range")))
} else {
let f = n.as_f64().unwrap_or(f64::NAN);
if f.is_finite() {
Ok(f.trunc() as i64)
} else {
Err(Error::Value(format!("cannot convert float {} to integer", repr(value))))
}
}
}
Value::Bool(b) => Ok(i64::from(*b)),
Value::String(s) => s
.trim()
.parse::<i64>()
.map_err(|_| Error::Value(format!("invalid literal for int() with base 10: {}", repr(value)))),
other => Err(Error::Type(format!(
"int() argument must be a string, a bytes-like object or a real number, not '{}'",
type_name(other)
))),
}
}
pub fn to_float(value: &Value) -> Result<f64> {
match value {
Value::Number(n) => Ok(n.as_f64().unwrap_or(f64::NAN)),
Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
Value::String(s) => {
let t = s.trim();
match t.to_ascii_lowercase().as_str() {
"nan" | "+nan" | "-nan" => Ok(f64::NAN),
"inf" | "+inf" | "infinity" | "+infinity" => Ok(f64::INFINITY),
"-inf" | "-infinity" => Ok(f64::NEG_INFINITY),
_ => t
.parse::<f64>()
.map_err(|_| Error::Value(format!("could not convert string to float: {}", repr(value)))),
}
}
other => {
Err(Error::Type(format!("float() argument must be a string or a real number, not '{}'", type_name(other))))
}
}
}
pub fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "NoneType",
Value::Bool(_) => "bool",
Value::Number(n) if n.is_f64() => "float",
Value::Number(_) => "int",
Value::String(_) => "str",
Value::Array(_) => "list",
Value::Object(_) => "dict",
}
}
pub fn require<'a>(doc: &'a Map<String, Value>, key: &str) -> Result<&'a Value> {
doc.get(key).ok_or_else(|| Error::Key(crate::json::repr_str(key)))
}
pub fn get<'a>(doc: &'a Map<String, Value>, key: &str) -> Option<&'a Value> {
match doc.get(key) {
Some(Value::Null) | None => None,
Some(v) => Some(v),
}
}
pub fn get_str(doc: &Map<String, Value>, key: &str) -> Option<String> {
get(doc, key).map(to_str)
}
pub fn get_number(doc: &Map<String, Value>, key: &str) -> Result<Option<Number>> {
match get(doc, key) {
None => Ok(None),
Some(Value::Number(n)) => Ok(Some(n.clone())),
Some(other) => Ok(Some(float_number(to_float(other)?))),
}
}
pub fn get_list<'a>(doc: &'a Map<String, Value>, key: &str) -> &'a [Value] {
match doc.get(key) {
Some(Value::Array(items)) => items,
_ => &[],
}
}
pub fn as_object<'a>(value: &'a Value, what: &str) -> Result<&'a Map<String, Value>> {
value.as_object().ok_or_else(|| Error::Type(format!("{what} must be a JSON object, not {}", type_name(value))))
}
pub fn float_number(value: f64) -> Number {
Number::from_f64(value).unwrap_or_else(|| Number::from(0))
}
pub fn truthy(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
}
}
pub fn title_case(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut previous_cased = false;
for ch in text.chars() {
if ch.is_alphabetic() {
if previous_cased {
out.extend(ch.to_lowercase());
} else {
out.extend(ch.to_uppercase());
}
previous_cased = true;
} else {
out.push(ch);
previous_cased = false;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn coercions_follow_python() {
assert_eq!(to_str(&json!(123)), "123");
assert_eq!(to_str(&json!(1.5)), "1.5");
assert_eq!(to_int(&json!(5.9)).unwrap(), 5);
assert_eq!(to_int(&json!("7")).unwrap(), 7);
assert_eq!(to_float(&json!(2)).unwrap(), 2.0);
assert_eq!(title_case("left_kidney"), "Left_Kidney");
assert_eq!(title_case("left kidney"), "Left Kidney");
assert_eq!(title_case("a1b"), "A1B");
}
}
pub fn py_path(path: &std::path::Path) -> String {
let text = path.to_string_lossy();
#[cfg(windows)]
let text = text.replace('\\', "/");
let root = if text.starts_with("//") && !text.starts_with("///") {
"//"
} else if text.starts_with('/') {
"/"
} else {
""
};
let parts: Vec<&str> = text.split('/').filter(|p| !p.is_empty() && *p != ".").collect();
match (root, parts.is_empty()) {
("", true) => ".".to_string(),
(r, _) => format!("{r}{}", parts.join("/")),
}
}
#[cfg(test)]
mod path_tests {
use super::py_path;
use std::path::Path;
#[test]
fn paths_print_as_pathlib_prints_them() {
for (raw, shown) in [
("./corpus/", "corpus"),
("corpus/expected.json", "corpus/expected.json"),
("", "."),
(".", "."),
("/tmp//x/./y/", "/tmp/x/y"),
("//host/share", "//host/share"),
("///a", "/a"),
("../up/./x", "../up/x"),
("/", "/"),
] {
assert_eq!(py_path(Path::new(raw)), shown, "{raw:?}");
}
}
}