jsonschema-rs/python/src/lib.rs

155 lines
4.6 KiB
Rust

#![feature(core_intrinsics)]
#![warn(
clippy::doc_markdown,
clippy::redundant_closure,
clippy::explicit_iter_loop,
clippy::match_same_arms,
clippy::needless_borrow,
clippy::print_stdout,
clippy::integer_arithmetic,
clippy::cast_possible_truncation,
clippy::result_unwrap_used,
clippy::result_map_unwrap_or_else,
clippy::option_unwrap_used,
clippy::option_map_unwrap_or_else,
clippy::option_map_unwrap_or
)]
use jsonschema::Draft;
use pyo3::prelude::*;
use pyo3::types::PyAny;
use pyo3::{exceptions, wrap_pyfunction, PyObjectProtocol};
use serde_json::Value;
mod ser;
mod string;
mod types;
const MODULE_DOCSTRING: &str = "JSON Schema validation for Python written in Rust.";
const DRAFT7: u8 = 7;
const DRAFT6: u8 = 6;
const DRAFT4: u8 = 4;
#[derive(Debug)]
enum JSONSchemaError {
Compilation(jsonschema::CompilationError),
}
impl From<JSONSchemaError> for PyErr {
fn from(error: JSONSchemaError) -> PyErr {
exceptions::ValueError::py_err(match error {
JSONSchemaError::Compilation(_) => "Invalid schema",
})
}
}
fn get_draft(draft: Option<u8>) -> PyResult<Draft> {
if let Some(value) = draft {
match value {
DRAFT4 => Ok(jsonschema::Draft::Draft4),
DRAFT6 => Ok(jsonschema::Draft::Draft6),
DRAFT7 => Ok(jsonschema::Draft::Draft7),
_ => Err(exceptions::ValueError::py_err(format!(
"Unknown draft: {}",
value
))),
}
} else {
Ok(jsonschema::Draft::default())
}
}
/// A shortcut for validating the input instance against the schema.
///
/// >>> is_valid({"minimum": 5}, 3)
/// False
///
/// If your workflow implies validating against the same schema, consider using `JSONSchema.is_valid`
/// instead.
#[pyfunction]
#[text_signature = "(schema, instance, draft=None)"]
fn is_valid(schema: &PyAny, instance: &PyAny, draft: Option<u8>) -> PyResult<bool> {
let draft = get_draft(draft).map(Some)?;
let schema = ser::to_value(schema)?;
let instance = ser::to_value(instance)?;
let compiled =
jsonschema::JSONSchema::compile(&schema, draft).map_err(JSONSchemaError::Compilation)?;
Ok(compiled.is_valid(&instance))
}
/// JSON Schema compiled into a validation tree.
///
/// >>> compiled = JSONSchema({"minimum": 5})
/// >>> compiled.is_valid(3)
/// False
///
/// By default Draft 7 will be used for compilation.
#[pyclass]
#[text_signature = "(schema, draft=None)"]
struct JSONSchema {
schema: jsonschema::JSONSchema<'static>,
raw_schema: &'static Value,
}
#[pymethods]
impl JSONSchema {
#[new]
fn new(schema: &PyAny, draft: Option<u8>) -> PyResult<Self> {
let draft = get_draft(draft).map(Some)?;
let raw_schema = ser::to_value(schema)?;
// Currently, it is the simplest way to pass a reference to `JSONSchema`
// It is cleaned up in the `Drop` implementation
let schema: &'static Value = Box::leak(Box::new(raw_schema));
Ok(JSONSchema {
schema: jsonschema::JSONSchema::compile(schema, draft)
.map_err(JSONSchemaError::Compilation)?,
raw_schema: schema,
})
}
/// Perform fast validation against the compiled schema.
///
/// >>> compiled = JSONSchema({"minimum": 5})
/// >>> compiled.is_valid(3)
/// False
///
/// The output is a boolean value, that indicates whether the instance is valid or not.
#[text_signature = "(instance)"]
fn is_valid(&self, instance: &PyAny) -> bool {
let instance = ser::to_value(instance).unwrap();
self.schema.is_valid(&instance)
}
}
const SCHEMA_LENGTH_LIMIT: usize = 32;
#[pyproto]
impl<'p> PyObjectProtocol<'p> for JSONSchema {
fn __repr__(&self) -> PyResult<String> {
let mut schema = self.raw_schema.to_string();
if schema.len() > SCHEMA_LENGTH_LIMIT {
schema.truncate(SCHEMA_LENGTH_LIMIT);
schema = format!("{}...}}", schema);
}
Ok(format!("<JSONSchema: {}>", schema))
}
}
impl Drop for JSONSchema {
fn drop(&mut self) {
// Since `self.raw_schema` is not used anywhere else, there should be no double-free
unsafe { Box::from_raw(self.raw_schema as *const _ as *mut Value) };
}
}
#[pymodule]
fn jsonschema_rs(_py: Python, module: &PyModule) -> PyResult<()> {
types::init();
module.add_wrapped(wrap_pyfunction!(is_valid))?;
module.add_class::<JSONSchema>()?;
module.add("Draft4", DRAFT4)?;
module.add("Draft6", DRAFT6)?;
module.add("Draft7", DRAFT7)?;
module.add("__doc__", MODULE_DOCSTRING)?;
Ok(())
}