#![warn(missing_docs)]
use std::collections::{BTreeMap, HashMap};
mod data;
mod decode;
mod encode;
mod validate;
#[allow(missing_docs)]
mod schema {
capnp::generated_code!(pub mod json_capnp);
}
#[allow(missing_docs)]
mod rust_json_schema {
capnp::generated_code!(pub mod rust_json_capnp);
}
#[doc(hidden)]
pub use rust_json_schema::rust_json_capnp;
#[doc(hidden)]
pub use schema::json_capnp;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum DataEncoding {
#[default]
Default,
Base64,
Hex,
}
struct EncodingOptions<'schema, 'prefix> {
prefix: &'prefix std::borrow::Cow<'schema, str>,
name: &'schema str,
field: Option<capnp::schema::Field>,
flatten: Option<json_capnp::flatten_options::Reader<'schema>>,
discriminator: Option<json_capnp::discriminator_options::Reader<'schema>>,
data_encoding: DataEncoding,
codec: Option<&'schema str>,
}
impl Default for EncodingOptions<'_, '_> {
fn default() -> Self {
Self {
prefix: &std::borrow::Cow::Borrowed(""),
name: "",
field: None,
flatten: None,
discriminator: None,
data_encoding: DataEncoding::Default,
codec: None,
}
}
}
impl<'schema, 'prefix> EncodingOptions<'schema, 'prefix> {
fn from_field(
prefix: &'prefix std::borrow::Cow<'schema, str>,
field: capnp::schema::Field,
) -> capnp::Result<Self> {
let mut options = Self {
prefix,
name: field.get_proto().get_name()?.to_str()?,
field: Some(field),
flatten: None,
discriminator: None,
data_encoding: DataEncoding::Default,
codec: None,
};
for anno in field.get_annotations()?.iter() {
match anno.get_id() {
rust_json_capnp::codec::ID => {
options.codec = Some(
anno
.get_value()?
.downcast::<capnp::text::Reader>()
.to_str()?,
);
}
json_capnp::name::ID => {
options.name = anno
.get_value()?
.downcast::<capnp::text::Reader>()
.to_str()?;
}
json_capnp::base64::ID => {
if options.data_encoding != DataEncoding::Default {
return Err(capnp::Error::failed(
"Cannot specify both base64 and hex annotations on the same field"
.into(),
));
}
options.data_encoding = DataEncoding::Base64;
}
json_capnp::hex::ID => {
if options.data_encoding != DataEncoding::Default {
return Err(capnp::Error::failed(
"Cannot specify both base64 and hex annotations on the same field"
.into(),
));
}
options.data_encoding = DataEncoding::Hex;
}
json_capnp::flatten::ID => {
options.flatten = Some(
anno
.get_value()?
.downcast_struct::<json_capnp::flatten_options::Owned>(),
);
}
json_capnp::discriminator::ID => {
options.discriminator = Some(
anno
.get_value()?
.downcast_struct::<json_capnp::discriminator_options::Owned>(),
);
}
_ => {}
}
}
if options.data_encoding != DataEncoding::Default {
let mut element_type = field.get_type();
while let capnp::introspect::TypeVariant::List(sub_element_type) =
element_type.which()
{
element_type = sub_element_type;
}
if !matches!(element_type.which(), capnp::introspect::TypeVariant::Data) {
return Err(capnp::Error::failed(
"base64/hex annotation can only be applied to Data fields".into(),
));
}
}
Ok(options)
}
}
pub fn validate_schema<T: capnp::traits::OwnedStruct>() -> capnp::Result<()> {
let capnp::introspect::TypeVariant::Struct(raw) = T::introspect().which()
else {
return Err(capnp::Error::failed(
"validate_schema requires a struct type".into(),
));
};
validate::check_flattening_terminates(capnp::schema::StructSchema::new(raw))
}
pub fn to_json<'msg>(
reader: impl Into<capnp::dynamic_value::Reader<'msg>>,
) -> capnp::Result<String> {
Codec::new().encode(reader)
}
pub fn from_json<'segments>(
json: &str,
builder: impl Into<capnp::dynamic_value::Builder<'segments>>,
) -> capnp::Result<()> {
Codec::new().decode(json, builder)
}
#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(BTreeMap<String, JsonValue>),
#[doc(hidden)]
DataBuffer(Vec<u8>),
}
pub trait FieldCodec {
fn encode_value(
&self,
source: capnp::dynamic_value::Reader<'_>,
) -> capnp::Result<JsonValue>;
fn decode_value(
&self,
source: &JsonValue,
target: capnp::dynamic_value::Builder<'_>,
) -> capnp::Result<()>;
fn decode_member(
&self,
source: &JsonValue,
target: capnp::dynamic_struct::Builder<'_>,
field: capnp::schema::Field,
) -> capnp::Result<()> {
self.decode_value(source, target.init(field)?)
}
}
impl<T: FieldCodec + ?Sized> FieldCodec for &T {
fn encode_value(
&self,
source: capnp::dynamic_value::Reader<'_>,
) -> capnp::Result<JsonValue> {
(**self).encode_value(source)
}
fn decode_value(
&self,
source: &JsonValue,
target: capnp::dynamic_value::Builder<'_>,
) -> capnp::Result<()> {
(**self).decode_value(source, target)
}
fn decode_member(
&self,
source: &JsonValue,
target: capnp::dynamic_struct::Builder<'_>,
field: capnp::schema::Field,
) -> capnp::Result<()> {
(**self).decode_member(source, target, field)
}
}
impl<F, G> FieldCodec for (F, G)
where
F: Fn(capnp::dynamic_value::Reader<'_>) -> capnp::Result<JsonValue>,
G: Fn(&JsonValue, capnp::dynamic_value::Builder<'_>) -> capnp::Result<()>,
{
fn encode_value(
&self,
source: capnp::dynamic_value::Reader<'_>,
) -> capnp::Result<JsonValue> {
(self.0)(source)
}
fn decode_value(
&self,
source: &JsonValue,
target: capnp::dynamic_value::Builder<'_>,
) -> capnp::Result<()> {
(self.1)(source, target)
}
}
pub fn make_field_codec<'env>(
encode_fn: impl Fn(capnp::dynamic_value::Reader<'_>) -> capnp::Result<JsonValue>
+ 'env,
decode_fn: impl Fn(&JsonValue, capnp::dynamic_value::Builder<'_>) -> capnp::Result<()>
+ 'env,
) -> impl FieldCodec + 'env {
(encode_fn, decode_fn)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodecOptions {
pub recursion_limit: usize,
}
impl Default for CodecOptions {
fn default() -> Self {
Self {
recursion_limit: 64,
}
}
}
pub struct Codec<'env> {
field_overrides: HashMap<capnp::schema::Field, Box<dyn FieldCodec + 'env>>,
type_overrides: HashMap<capnp::introspect::Type, Box<dyn FieldCodec + 'env>>,
registry: HashMap<String, Box<dyn FieldCodec + 'env>>,
options: CodecOptions,
}
impl<'env> Codec<'env> {
pub fn new() -> Self {
Self::new_with_options(CodecOptions::default())
}
pub fn new_with_options(options: CodecOptions) -> Self {
Self {
field_overrides: HashMap::new(),
type_overrides: HashMap::new(),
registry: HashMap::new(),
options,
}
}
pub fn with_field_override(
mut self,
field: capnp::schema::Field,
codec: impl FieldCodec + 'env,
) -> Self {
self.field_overrides.insert(field, Box::new(codec));
self
}
pub fn with_type_override(
mut self,
typ: capnp::introspect::Type,
codec: impl FieldCodec + 'env,
) -> Self {
self.type_overrides.insert(typ, Box::new(codec));
self
}
pub fn with_named_codec(
mut self,
name: impl Into<String>,
codec: impl FieldCodec + 'env,
) -> Self {
self.registry.insert(name.into(), Box::new(codec));
self
}
pub fn encode<'msg>(
&self,
reader: impl Into<capnp::dynamic_value::Reader<'msg>>,
) -> capnp::Result<String> {
let mut writer = std::io::Cursor::new(Vec::with_capacity(4096));
self.encode_to(&mut writer, reader)?;
String::from_utf8(writer.into_inner()).map_err(|e| {
capnp::Error::failed(format!(
"Failed to convert JSON bytes to string: {}",
e
))
})
}
pub fn encode_to<'msg, W: std::io::Write>(
&self,
writer: &mut W,
reader: impl Into<capnp::dynamic_value::Reader<'msg>>,
) -> capnp::Result<()> {
let capnp::dynamic_value::Reader::Struct(reader) = reader.into() else {
return Err(capnp::Error::failed(
"Top-level value must be a struct".into(),
));
};
encode::serialize_json_to(self, writer, reader)
}
pub fn decode<'segments>(
&self,
json: &str,
builder: impl Into<capnp::dynamic_value::Builder<'segments>>,
) -> capnp::Result<()> {
let capnp::dynamic_value::Builder::Struct(builder) = builder.into() else {
return Err(capnp::Error::failed(
"Top-level JSON value must be an object".into(),
));
};
decode::parse(self, json, builder)
}
}
impl Default for Codec<'_> {
fn default() -> Self {
Self::new()
}
}