Skip to content

Commit

Permalink
Merge pull request #102 from y-crdt/to_json
Browse files Browse the repository at this point in the history
`to_json` compatibility with Python `json`
  • Loading branch information
Waidhoferj authored Dec 19, 2022
2 parents f6ccd79 + 4cfb192 commit f24b058
Show file tree
Hide file tree
Showing 11 changed files with 560 additions and 286 deletions.
152 changes: 152 additions & 0 deletions src/json_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::{collections::HashMap, convert::TryFrom};

use lib0::any::Any;
use pyo3::{exceptions::PyTypeError, PyErr, PyObject, PyResult, Python};

use crate::shared_types::{CompatiblePyType, YPyType};

#[derive(Clone, Debug)]
pub(crate) struct JsonBuilder(String);

impl JsonBuilder {
pub fn new() -> Self {
JsonBuilder(String::new())
}

pub fn append_json<T: JsonBuildable>(&mut self, buildable: &T) -> Result<(), T::JsonError> {
let buffer = &mut self.0;
buildable.build_json(buffer)
}
}

impl From<JsonBuilder> for String {
fn from(json_builder: JsonBuilder) -> Self {
json_builder.0
}
}

pub(crate) trait JsonBuildable {
type JsonError;
fn build_json(&self, buffer: &mut String) -> Result<(), Self::JsonError>;
}

impl<'a> JsonBuildable for CompatiblePyType<'a> {
type JsonError = PyErr;

fn build_json(&self, buffer: &mut String) -> Result<(), Self::JsonError> {
match self {
CompatiblePyType::Bool(b) => {
let t: bool = b.extract().unwrap();
buffer.push_str(if t { "true" } else { "false" });
}
CompatiblePyType::Int(i) => buffer.push_str(&i.to_string()),
CompatiblePyType::Float(f) => buffer.push_str(&f.to_string()),
CompatiblePyType::String(s) => {
let string: String = s.extract().unwrap();
buffer.reserve(string.len() + 2);
buffer.push_str("\"");
buffer.push_str(&string);
buffer.push_str("\"");
}
CompatiblePyType::List(list) => {
buffer.push_str("[");
let length = list.len();
for (i, element) in list.iter().enumerate() {
CompatiblePyType::try_from(element)?.build_json(buffer)?;
if i + 1 < length {
buffer.push_str(",");
}
}

buffer.push_str("]");
}
CompatiblePyType::Dict(dict) => {
buffer.push_str("{");
let length = dict.len();
for (i, (k, v)) in dict.iter().enumerate() {
CompatiblePyType::try_from(k)?.build_json(buffer)?;
buffer.push_str(":");
CompatiblePyType::try_from(v)?.build_json(buffer)?;
if i + 1 < length {
buffer.push_str(",");
}
}
buffer.push_str("}");
}
CompatiblePyType::YType(y_type) => y_type.build_json(buffer)?,
CompatiblePyType::None => buffer.push_str("null"),
}

Ok(())
}
}

impl<'a> JsonBuildable for YPyType<'a> {
type JsonError = PyErr;

fn build_json(&self, buffer: &mut String) -> Result<(), Self::JsonError> {
let json = match self {
YPyType::Text(text) => Ok(text.borrow().to_json()),
YPyType::Array(array) => array.borrow().to_json(),
YPyType::Map(map) => map.borrow().to_json(),
xml => Err(PyTypeError::new_err(format!(
"XML elements cannot be converted to a JSON format: {xml}"
))),
};
buffer.push_str(&json?);
Ok(())
}
}

impl JsonBuildable for Any {
type JsonError = PyErr;
fn build_json(&self, buffer: &mut String) -> Result<(), Self::JsonError> {
self.to_json(buffer);
Ok(())
}
}

impl JsonBuildable for HashMap<String, PyObject> {
type JsonError = PyErr;

fn build_json(&self, buffer: &mut String) -> Result<(), Self::JsonError> {
buffer.push_str("{");
let res: PyResult<()> = Python::with_gil(|py| {
for (i, (k, py_obj)) in self.iter().enumerate() {
let value: CompatiblePyType = py_obj.extract(py)?;
if i != 0 {
buffer.push_str(",");
}
buffer.push_str(k);
buffer.push_str(":");
value.build_json(buffer)?;
}
Ok(())
});
res?;

buffer.push_str("}");
Ok(())
}
}

impl JsonBuildable for Vec<PyObject> {
type JsonError = PyErr;

fn build_json(&self, buffer: &mut String) -> Result<(), Self::JsonError> {
buffer.push_str("[");
let res: PyResult<()> = Python::with_gil(|py| {
self.iter().enumerate().try_for_each(|(i, object)| {
let py_type: CompatiblePyType = object.extract(py)?;
if i != 0 {
buffer.push_str(",");
}
py_type.build_json(buffer)?;
Ok(())
})
});
res?;
buffer.push_str("]");
Ok(())
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
mod json_builder;
mod shared_types;
mod type_conversions;
mod y_array;
Expand Down
92 changes: 41 additions & 51 deletions src/shared_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use crate::{
y_xml::{YXmlElement, YXmlText},
};
use pyo3::create_exception;
use pyo3::types as pytypes;
use pyo3::{exceptions::PyException, prelude::*};
use std::{convert::TryFrom, fmt::Display};
use std::fmt::Display;
use yrs::types::TYPE_REFS_XML_TEXT;
use yrs::types::{TypeRefs, TYPE_REFS_ARRAY, TYPE_REFS_MAP, TYPE_REFS_TEXT};
use yrs::{types::TYPE_REFS_XML_ELEMENT, SubscriptionId};
Expand Down Expand Up @@ -50,6 +51,18 @@ pub enum SubId {
Deep(DeepSubscription),
}

#[derive(Clone)]
pub enum CompatiblePyType<'a> {
Bool(&'a pytypes::PyBool),
Int(&'a pytypes::PyInt),
Float(&'a pytypes::PyFloat),
String(&'a pytypes::PyString),
List(&'a pytypes::PyList),
Dict(&'a pytypes::PyDict),
YType(YPyType<'a>),
None,
}

#[derive(Clone)]
pub enum SharedType<I, P> {
Integrated(I),
Expand All @@ -67,68 +80,45 @@ impl<I, P> SharedType<I, P> {
SharedType::Prelim(prelim)
}
}

#[derive(FromPyObject)]
pub enum Shared {
Text(Py<YText>),
Array(Py<YArray>),
Map(Py<YMap>),
XmlElement(Py<YXmlElement>),
XmlText(Py<YXmlText>),
#[derive(Clone)]
pub enum YPyType<'a> {
Text(&'a PyCell<YText>),
Array(&'a PyCell<YArray>),
Map(&'a PyCell<YMap>),
XmlElement(&'a PyCell<YXmlElement>),
XmlText(&'a PyCell<YXmlText>),
}

impl Shared {
impl<'a> YPyType<'a> {
pub fn is_prelim(&self) -> bool {
Python::with_gil(|py| match self {
Shared::Text(v) => v.borrow(py).prelim(),
Shared::Array(v) => v.borrow(py).prelim(),
Shared::Map(v) => v.borrow(py).prelim(),
Shared::XmlElement(_) | Shared::XmlText(_) => false,
})
match self {
YPyType::Text(v) => v.borrow().prelim(),
YPyType::Array(v) => v.borrow().prelim(),
YPyType::Map(v) => v.borrow().prelim(),
YPyType::XmlElement(_) | YPyType::XmlText(_) => false,
}
}

pub fn type_ref(&self) -> TypeRefs {
match self {
Shared::Text(_) => TYPE_REFS_TEXT,
Shared::Array(_) => TYPE_REFS_ARRAY,
Shared::Map(_) => TYPE_REFS_MAP,
Shared::XmlElement(_) => TYPE_REFS_XML_ELEMENT,
Shared::XmlText(_) => TYPE_REFS_XML_TEXT,
YPyType::Text(_) => TYPE_REFS_TEXT,
YPyType::Array(_) => TYPE_REFS_ARRAY,
YPyType::Map(_) => TYPE_REFS_MAP,
YPyType::XmlElement(_) => TYPE_REFS_XML_ELEMENT,
YPyType::XmlText(_) => TYPE_REFS_XML_TEXT,
}
}
}

impl Display for Shared {
impl<'a> Display for YPyType<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let info = Python::with_gil(|py| match self {
Shared::Text(t) => t.borrow(py).__str__(),
Shared::Array(a) => a.borrow(py).__str__(),
Shared::Map(m) => m.borrow(py).__str__(),
Shared::XmlElement(xml) => xml.borrow(py).__str__(),
Shared::XmlText(xml) => xml.borrow(py).__str__(),
});
let info = match self {
YPyType::Text(t) => t.borrow().__str__(),
YPyType::Array(a) => a.borrow().__str__(),
YPyType::Map(m) => m.borrow().__str__(),
YPyType::XmlElement(xml) => xml.borrow().__str__(),
YPyType::XmlText(xml) => xml.borrow().__str__(),
};
write!(f, "{}", info)
}
}

impl TryFrom<PyObject> for Shared {
type Error = PyErr;

fn try_from(value: PyObject) -> Result<Self, Self::Error> {
Python::with_gil(|py| {
let value = value.as_ref(py);

if let Ok(text) = value.extract() {
Ok(Shared::Text(text))
} else if let Ok(array) = value.extract() {
Ok(Shared::Array(array))
} else if let Ok(map) = value.extract() {
Ok(Shared::Map(map))
} else {
Err(pyo3::exceptions::PyValueError::new_err(
"Could not extract Python value into a shared type.",
))
}
})
}
}
Loading

0 comments on commit f24b058

Please sign in to comment.