use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use lerna::config::value::{ConfigDict, ConfigValue};
use lerna::validation::{ConfigSchema, TypeSpec};
#[pyclass(name = "TypeSpec")]
#[derive(Clone)]
pub struct PyTypeSpec {
inner: TypeSpec,
}
#[pymethods]
impl PyTypeSpec {
#[staticmethod]
fn parse(s: &str) -> Option<Self> {
TypeSpec::parse(s).map(|t| Self { inner: t })
}
#[staticmethod]
fn int() -> Self {
Self {
inner: TypeSpec::Int,
}
}
#[staticmethod]
fn string() -> Self {
Self {
inner: TypeSpec::String,
}
}
#[staticmethod]
fn bool() -> Self {
Self {
inner: TypeSpec::Bool,
}
}
#[staticmethod]
fn float() -> Self {
Self {
inner: TypeSpec::Float,
}
}
#[staticmethod]
fn any() -> Self {
Self {
inner: TypeSpec::Any,
}
}
#[staticmethod]
fn list(inner: &PyTypeSpec) -> Self {
Self {
inner: TypeSpec::List(Box::new(inner.inner.clone())),
}
}
#[staticmethod]
fn optional(inner: &PyTypeSpec) -> Self {
Self {
inner: TypeSpec::Optional(Box::new(inner.inner.clone())),
}
}
fn __repr__(&self) -> String {
format!("{:?}", self.inner)
}
}
#[pyclass(name = "ValidationError")]
#[derive(Clone)]
pub struct PyValidationError {
#[pyo3(get)]
pub path: String,
#[pyo3(get)]
pub message: String,
}
#[pymethods]
impl PyValidationError {
fn __repr__(&self) -> String {
format!("{}: {}", self.path, self.message)
}
fn __str__(&self) -> String {
format!("{}: {}", self.path, self.message)
}
}
#[pyclass(name = "ConfigSchema")]
#[derive(Clone)]
pub struct PyConfigSchema {
inner: ConfigSchema,
}
#[pymethods]
impl PyConfigSchema {
#[new]
fn new() -> Self {
Self {
inner: ConfigSchema::new(),
}
}
fn required(&mut self, name: &str, type_spec: &PyTypeSpec) {
self.inner
.fields
.insert(name.to_string(), (type_spec.inner.clone(), true, None));
}
fn optional(
&mut self,
name: &str,
type_spec: &PyTypeSpec,
default: &Bound<'_, PyAny>,
) -> PyResult<()> {
let default_value = py_to_config_value(default)?;
self.inner.fields.insert(
name.to_string(),
(type_spec.inner.clone(), false, Some(default_value)),
);
Ok(())
}
fn validate(&self, config: &Bound<'_, PyDict>) -> PyResult<Vec<PyValidationError>> {
let config_dict = py_dict_to_config_dict(config)?;
match self.inner.validate(&config_dict) {
Ok(()) => Ok(vec![]),
Err(errors) => Ok(errors
.into_iter()
.map(|e| PyValidationError {
path: e.path,
message: e.message,
})
.collect()),
}
}
fn is_valid(&self, config: &Bound<'_, PyDict>) -> PyResult<bool> {
let config_dict = py_dict_to_config_dict(config)?;
Ok(self.inner.validate(&config_dict).is_ok())
}
fn __repr__(&self) -> String {
format!("ConfigSchema({} fields)", self.inner.fields.len())
}
}
fn py_to_config_value(obj: &Bound<'_, PyAny>) -> PyResult<ConfigValue> {
if obj.is_none() {
Ok(ConfigValue::Null)
} else if let Ok(b) = obj.extract::<bool>() {
Ok(ConfigValue::Bool(b))
} else if let Ok(i) = obj.extract::<i64>() {
Ok(ConfigValue::Int(i))
} else if let Ok(f) = obj.extract::<f64>() {
Ok(ConfigValue::Float(f))
} else if let Ok(s) = obj.extract::<String>() {
Ok(ConfigValue::String(s))
} else if let Ok(list) = obj.cast::<PyList>() {
let mut items = Vec::new();
for item in list.iter() {
items.push(py_to_config_value(&item)?);
}
Ok(ConfigValue::List(items))
} else if let Ok(dict) = obj.cast::<PyDict>() {
let config_dict = py_dict_to_config_dict(dict)?;
Ok(ConfigValue::Dict(config_dict))
} else {
Ok(ConfigValue::String(obj.str()?.to_string()))
}
}
fn py_dict_to_config_dict(dict: &Bound<'_, PyDict>) -> PyResult<ConfigDict> {
let mut config_dict = ConfigDict::new();
for (key, value) in dict.iter() {
if let Ok(k) = key.extract::<String>() {
config_dict.insert(k, py_to_config_value(&value)?);
}
}
Ok(config_dict)
}
#[pyfunction]
fn validate_type(value: &Bound<'_, PyAny>, type_spec: &PyTypeSpec) -> PyResult<bool> {
let config_value = py_to_config_value(value)?;
Ok(type_spec.inner.matches(&config_value))
}
pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> {
let m = PyModule::new(parent.py(), "validation")?;
m.add_class::<PyTypeSpec>()?;
m.add_class::<PyValidationError>()?;
m.add_class::<PyConfigSchema>()?;
m.add_function(wrap_pyfunction!(validate_type, &m)?)?;
parent.add_submodule(&m)?;
Ok(())
}