chore: extract arbitrary
into its own crate arbitrary-value
This commit is contained in:
17
arbitrary-value/Cargo.toml
Normal file
17
arbitrary-value/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "arbitrary-value"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
pyo3 = ["dep:pyo3"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
ijson = "0.1.4"
|
||||
itertools = "0.14.0"
|
||||
snafu = { workspace = true }
|
||||
|
||||
pyo3 = { workspace = true, optional = true, features = ["chrono", "chrono-tz"] }
|
116
arbitrary-value/src/arbitrary.rs
Normal file
116
arbitrary-value/src/arbitrary.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use chrono::DateTime;
|
||||
use chrono_tz::Tz;
|
||||
use ijson::{IArray, INumber, IObject, IString, IValue};
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::{
|
||||
exceptions::{PyTypeError, PyValueError},
|
||||
prelude::*,
|
||||
types::{PyList, PyNone},
|
||||
};
|
||||
use snafu::Snafu;
|
||||
|
||||
use super::{finite_f64::FiniteF64, map::Map, map_key::MapKey};
|
||||
|
||||
#[derive(Debug, Clone, derive_more::From, derive_more::TryInto)]
|
||||
pub enum Arbitrary {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Integer(i64),
|
||||
Float(FiniteF64),
|
||||
String(String),
|
||||
Array(Vec<Arbitrary>),
|
||||
Map(Map),
|
||||
DateTime(DateTime<Tz>),
|
||||
}
|
||||
|
||||
impl From<MapKey> for Arbitrary {
|
||||
fn from(map_key: MapKey) -> Self {
|
||||
match map_key {
|
||||
MapKey::Null => Arbitrary::Null,
|
||||
MapKey::Bool(b) => Arbitrary::Bool(b),
|
||||
MapKey::Integer(int) => Arbitrary::Integer(int),
|
||||
MapKey::String(s) => Arbitrary::String(s),
|
||||
// close enough
|
||||
MapKey::Tuple(vec) => Arbitrary::Array(vec.into_iter().map(Into::into).collect()),
|
||||
MapKey::DateTime(date_time) => Arbitrary::DateTime(date_time),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
pub enum MapKeyFromArbitraryError {
|
||||
#[snafu(display("floats aren't supported as map keys yet. got {value:?}"))]
|
||||
FloatNotSupported { value: FiniteF64 },
|
||||
#[snafu(display("a map cannot be a map key. got {value:?}"))]
|
||||
MapCannotBeAMapKey { value: Map },
|
||||
}
|
||||
|
||||
impl From<Arbitrary> for IValue {
|
||||
fn from(value: Arbitrary) -> Self {
|
||||
match value {
|
||||
Arbitrary::Null => IValue::NULL,
|
||||
Arbitrary::Bool(true) => IValue::TRUE,
|
||||
Arbitrary::Bool(false) => IValue::FALSE,
|
||||
Arbitrary::Integer(int) => INumber::from(int).into(),
|
||||
Arbitrary::Float(float) => INumber::try_from(f64::from(float)).unwrap().into(),
|
||||
Arbitrary::String(s) => IString::from(s).into(),
|
||||
Arbitrary::Array(vec) => {
|
||||
IArray::from_iter(vec.into_iter().map(Into::<IValue>::into)).into()
|
||||
}
|
||||
Arbitrary::Map(Map(btree_map)) => {
|
||||
let mut object = IObject::new();
|
||||
|
||||
for (key, value) in btree_map {
|
||||
let key: IString = key.into();
|
||||
object.insert(key, value);
|
||||
}
|
||||
|
||||
object.into()
|
||||
}
|
||||
Arbitrary::DateTime(date_time) => IString::from(date_time.to_rfc3339()).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> FromPyObject<'py> for Arbitrary {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
if let Ok(map_key) = ob.extract::<MapKey>() {
|
||||
Ok(map_key.into())
|
||||
} else if let Ok(map) = ob.extract() {
|
||||
Ok(Self::Map(map))
|
||||
} else if let Ok(f) = ob.extract::<f64>() {
|
||||
let f = FiniteF64::try_from(f).map_err(|err| PyValueError::new_err(err.to_string()))?;
|
||||
Ok(Self::Float(f))
|
||||
} else if let Ok(vec) = ob.extract() {
|
||||
Ok(Self::Array(vec))
|
||||
} else {
|
||||
let type_name = ob.get_type().fully_qualified_name()?;
|
||||
Err(PyTypeError::new_err(format!(
|
||||
"can't extract an arbitrary from a {type_name}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> IntoPyObject<'py> for Arbitrary {
|
||||
type Target = PyAny;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = PyErr;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
Arbitrary::Null => Ok(PyNone::get(py).to_owned().into_any()),
|
||||
Arbitrary::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any()),
|
||||
Arbitrary::Integer(i) => Ok(i.into_pyobject(py)?.into_any()),
|
||||
Arbitrary::Float(finite_f64) => Ok(finite_f64.into_pyobject(py)?.into_any()),
|
||||
Arbitrary::String(s) => Ok(s.into_pyobject(py)?.into_any()),
|
||||
Arbitrary::Array(vec) => Ok(PyList::new(py, vec)?.into_any()),
|
||||
Arbitrary::Map(map) => Ok(map.into_pyobject(py)?.into_any()),
|
||||
Arbitrary::DateTime(date_time) => Ok(date_time.into_pyobject(py)?.into_any()),
|
||||
}
|
||||
}
|
||||
}
|
23
arbitrary-value/src/finite_f64.rs
Normal file
23
arbitrary-value/src/finite_f64.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use snafu::Snafu;
|
||||
|
||||
#[cfg_attr(feature = "pyo3", derive(pyo3::IntoPyObject))]
|
||||
#[derive(Debug, Clone, derive_more::Into)]
|
||||
pub struct FiniteF64(f64);
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
#[snafu(display("{value:?} is not finite"))]
|
||||
pub struct NotFinite {
|
||||
value: f64,
|
||||
}
|
||||
|
||||
impl TryFrom<f64> for FiniteF64 {
|
||||
type Error = NotFinite;
|
||||
|
||||
fn try_from(value: f64) -> Result<Self, Self::Error> {
|
||||
if value.is_finite() {
|
||||
Ok(Self(value))
|
||||
} else {
|
||||
Err(NotFinite { value })
|
||||
}
|
||||
}
|
||||
}
|
6
arbitrary-value/src/lib.rs
Normal file
6
arbitrary-value/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod arbitrary;
|
||||
pub mod finite_f64;
|
||||
pub mod map;
|
||||
pub mod map_key;
|
||||
|
||||
pub use arbitrary::*;
|
7
arbitrary-value/src/map.rs
Normal file
7
arbitrary-value/src/map.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{arbitrary::Arbitrary, map_key::MapKey};
|
||||
|
||||
#[cfg_attr(feature = "pyo3", derive(pyo3::FromPyObject, pyo3::IntoPyObject))]
|
||||
#[derive(Debug, Clone, Default, derive_more::From, derive_more::Into)]
|
||||
pub struct Map(pub BTreeMap<MapKey, Arbitrary>);
|
113
arbitrary-value/src/map_key.rs
Normal file
113
arbitrary-value/src/map_key.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono_tz::Tz;
|
||||
use ijson::IString;
|
||||
use itertools::Itertools;
|
||||
#[cfg(feature = "pyo3")]
|
||||
use pyo3::{
|
||||
exceptions::PyTypeError,
|
||||
prelude::*,
|
||||
types::{PyNone, PyTuple},
|
||||
};
|
||||
|
||||
use super::arbitrary::{Arbitrary, MapKeyFromArbitraryError};
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum MapKey {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Integer(i64),
|
||||
String(String),
|
||||
Tuple(Vec<MapKey>),
|
||||
DateTime(DateTime<Tz>),
|
||||
}
|
||||
|
||||
impl Display for MapKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MapKey::Null => write!(f, "null"),
|
||||
MapKey::Bool(b) => write!(f, "{b}"),
|
||||
MapKey::Integer(i) => write!(f, "{i}"),
|
||||
MapKey::String(s) => write!(f, "{s}"),
|
||||
MapKey::Tuple(vec) => {
|
||||
let comma_separated =
|
||||
Itertools::intersperse(vec.iter().map(ToString::to_string), ", ".to_string());
|
||||
write!(f, "({})", String::from_iter(comma_separated))
|
||||
}
|
||||
MapKey::DateTime(date_time) => {
|
||||
write!(f, "{}", date_time.to_rfc3339())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> FromPyObject<'py> for MapKey {
|
||||
fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
if let Ok(_none) = ob.downcast::<PyNone>() {
|
||||
Ok(Self::Null)
|
||||
} else if let Ok(b) = ob.extract() {
|
||||
Ok(Self::Bool(b))
|
||||
} else if let Ok(int) = ob.extract() {
|
||||
Ok(Self::Integer(int))
|
||||
} else if let Ok(s) = ob.extract() {
|
||||
Ok(Self::String(s))
|
||||
} else if let Ok(tuple) = ob.extract() {
|
||||
Ok(Self::Tuple(tuple))
|
||||
} else {
|
||||
let type_name = ob.get_type().fully_qualified_name()?;
|
||||
Err(PyTypeError::new_err(format!(
|
||||
"can't extract a map key from a {type_name}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pyo3")]
|
||||
impl<'py> IntoPyObject<'py> for MapKey {
|
||||
type Target = PyAny;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = PyErr;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
MapKey::Null => Ok(PyNone::get(py).to_owned().into_any()),
|
||||
MapKey::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any()),
|
||||
MapKey::Integer(i) => Ok(i.into_pyobject(py)?.into_any()),
|
||||
MapKey::String(s) => Ok(s.into_pyobject(py)?.into_any()),
|
||||
MapKey::Tuple(vec) => Ok(PyTuple::new(py, vec)?.into_any()),
|
||||
MapKey::DateTime(date_time) => Ok(date_time.into_pyobject(py)?.into_any()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MapKey> for IString {
|
||||
fn from(map_key: MapKey) -> Self {
|
||||
Self::from(map_key.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Arbitrary> for MapKey {
|
||||
type Error = MapKeyFromArbitraryError;
|
||||
|
||||
fn try_from(arbitrary: Arbitrary) -> Result<Self, Self::Error> {
|
||||
match arbitrary {
|
||||
Arbitrary::Null => Ok(MapKey::Null),
|
||||
Arbitrary::Bool(b) => Ok(MapKey::Bool(b)),
|
||||
Arbitrary::Integer(i) => Ok(MapKey::Integer(i)),
|
||||
Arbitrary::String(s) => Ok(MapKey::String(s)),
|
||||
Arbitrary::Array(vec) => {
|
||||
let tuple = Result::from_iter(vec.into_iter().map(TryInto::try_into))?;
|
||||
Ok(MapKey::Tuple(tuple))
|
||||
}
|
||||
Arbitrary::DateTime(date_time) => Ok(MapKey::DateTime(date_time)),
|
||||
Arbitrary::Float(float) => {
|
||||
Err(MapKeyFromArbitraryError::FloatNotSupported { value: float })
|
||||
}
|
||||
Arbitrary::Map(map) => Err(MapKeyFromArbitraryError::MapCannotBeAMapKey { value: map }),
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user