#[cfg(feature = "compression")]
use crate::calconfig::CompressionType;
use crate::calconfig::{CalConfig, ExternalizerConfig, SerializationFormat};
use crate::uci::{CalError, CalErrorKind, CalMessage, CalResult};
use std::collections::HashMap;
use std::io::{Read, Write};
pub trait Externalizer: Send + Sync {
fn encode(&self, bytes: &[u8]) -> CalResult<Vec<u8>> {
Ok(bytes.to_vec())
}
fn decode(&self, bytes: &[u8]) -> CalResult<Vec<u8>> {
Ok(bytes.to_vec())
}
fn next(&self) -> Option<&dyn Externalizer> {
None
}
fn serialization_format(&self) -> Option<SerializationFormat> {
None
}
fn get_cal_api_version(&self) -> &str;
fn get_encoding(&self) -> &str;
fn get_schema_version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn get_vendor_version(&self) -> &str {
env!("CARGO_PKG_VERSION")
}
fn get_vendor(&self) -> &str {
XML_EXTERNALIZER_VENDOR
}
fn message_read_only(&self) -> bool {
false
}
fn message_write_only(&self) -> bool {
false
}
fn supports_object_read(&self) -> bool {
true
}
fn supports_object_write(&self) -> bool {
true
}
}
pub trait ExternalizerLoader: Send + Sync {
fn get_externalizer(
&self,
encoding: &str,
schema_version: &str,
vendor_version: &str,
) -> CalResult<Box<dyn Externalizer>>;
}
pub fn get_externalizer_loader() -> Box<dyn ExternalizerLoader> {
Box::new(XmlExternalizerLoader)
}
pub fn write_to_bytes<M>(ext: &dyn Externalizer, msg: &M, root: &str) -> CalResult<Vec<u8>>
where
M: CalMessage + serde::Serialize,
{
let format = find_format(ext).ok_or_else(|| {
CalError::new(
CalErrorKind::SerializationError,
"no serialization format in externalizer chain",
)
})?;
let raw = serialize_xml(msg, root, format)?;
encode_chain(ext, &raw)
}
pub fn write_to_string<M>(ext: &dyn Externalizer, msg: &M, root: &str) -> CalResult<String>
where
M: CalMessage + serde::Serialize,
{
write_to_bytes(ext, msg, root).and_then(|b| {
String::from_utf8(b)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))
})
}
pub fn write_to_writer<M>(
ext: &dyn Externalizer,
msg: &M,
root: &str,
writer: &mut dyn Write,
) -> CalResult<()>
where
M: CalMessage + serde::Serialize,
{
let bytes = write_to_bytes(ext, msg, root)?;
writer
.write_all(&bytes)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))
}
pub fn read_from_bytes<M>(ext: &dyn Externalizer, bytes: &[u8]) -> CalResult<M>
where
M: CalMessage + serde::de::DeserializeOwned,
{
let decoded = decode_chain(ext, bytes)?;
quick_xml::de::from_reader(decoded.as_slice())
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))
}
pub fn read_from_str<M>(ext: &dyn Externalizer, s: &str) -> CalResult<M>
where
M: CalMessage + serde::de::DeserializeOwned,
{
read_from_bytes(ext, s.as_bytes())
}
pub fn read_from_reader<M>(ext: &dyn Externalizer, reader: &mut dyn Read) -> CalResult<M>
where
M: CalMessage + serde::de::DeserializeOwned,
{
let mut buf = Vec::new();
reader
.read_to_end(&mut buf)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))?;
read_from_bytes(ext, &buf)
}
fn find_format(ext: &dyn Externalizer) -> Option<SerializationFormat> {
ext.serialization_format()
.or_else(|| ext.next().and_then(find_format))
}
fn encode_chain(ext: &dyn Externalizer, bytes: &[u8]) -> CalResult<Vec<u8>> {
let encoded = ext.encode(bytes)?;
match ext.next() {
Some(n) => encode_chain(n, &encoded),
None => Ok(encoded),
}
}
fn decode_chain(ext: &dyn Externalizer, bytes: &[u8]) -> CalResult<Vec<u8>> {
let bytes = match ext.next() {
Some(n) => decode_chain(n, bytes)?,
None => bytes.to_vec(),
};
ext.decode(&bytes)
}
fn serialize_xml<M: serde::Serialize>(
msg: &M,
root: &str,
format: SerializationFormat,
) -> CalResult<Vec<u8>> {
match format {
SerializationFormat::Xml => quick_xml::se::to_string_with_root(root, msg)
.map(String::into_bytes)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string())),
SerializationFormat::PrettyXml => {
let mut buf = String::new();
let mut ser = quick_xml::se::Serializer::with_root(&mut buf, Some(root))
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))?;
ser.indent(' ', 4);
serde::Serialize::serialize(msg, ser)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))?;
Ok(buf.into_bytes())
}
}
}
pub const XML_EXTERNALIZER_CAL_API_VERSION: &str = "2.5";
pub const XML_EXTERNALIZER_ENCODING: &str = "xml";
pub const XML_EXTERNALIZER_VENDOR: &str = "rcal";
pub struct XmlExternalizer {
format: SerializationFormat,
pub next: Option<Box<dyn Externalizer>>,
}
impl XmlExternalizer {
pub fn new(format: SerializationFormat) -> Self {
Self { format, next: None }
}
pub fn from_builder(b: &ExternalizerBuilder) -> CalResult<Box<dyn Externalizer>> {
Ok(Box::new(xml_from_options(&b.options)))
}
}
impl Externalizer for XmlExternalizer {
fn next(&self) -> Option<&dyn Externalizer> {
self.next.as_deref()
}
fn serialization_format(&self) -> Option<SerializationFormat> {
Some(self.format)
}
fn get_cal_api_version(&self) -> &str {
XML_EXTERNALIZER_CAL_API_VERSION
}
fn get_encoding(&self) -> &str {
XML_EXTERNALIZER_ENCODING
}
}
fn xml_from_options(options: &HashMap<String, String>) -> XmlExternalizer {
let pretty = options.get("pretty").is_some_and(|v| v == "true");
let format = if pretty {
SerializationFormat::PrettyXml
} else {
SerializationFormat::Xml
};
XmlExternalizer { format, next: None }
}
#[cfg(feature = "compression")]
pub struct CompressionExternalizer {
compression_type: CompressionType,
level: flate2::Compression,
pub next: Option<Box<dyn Externalizer>>,
}
#[cfg(feature = "compression")]
impl CompressionExternalizer {
pub fn new(
next: Option<Box<dyn Externalizer>>,
compression_type: CompressionType,
level: flate2::Compression,
) -> Self {
Self {
compression_type,
level,
next,
}
}
pub fn from_builder(
b: &ExternalizerBuilder,
next: Option<Box<dyn Externalizer>>,
) -> CalResult<Box<dyn Externalizer>> {
Ok(Box::new(compression_from_options(&b.options, next)))
}
}
#[cfg(feature = "compression")]
impl Externalizer for CompressionExternalizer {
fn encode(&self, bytes: &[u8]) -> CalResult<Vec<u8>> {
compress(bytes, self.level, &self.compression_type)
}
fn decode(&self, bytes: &[u8]) -> CalResult<Vec<u8>> {
decompress(bytes, &self.compression_type)
}
fn next(&self) -> Option<&dyn Externalizer> {
self.next.as_deref()
}
fn get_cal_api_version(&self) -> &str {
XML_EXTERNALIZER_CAL_API_VERSION
}
fn get_encoding(&self) -> &str {
self.compression_type.as_str()
}
}
#[cfg(feature = "compression")]
fn compression_from_options(
options: &HashMap<String, String>,
next: Option<Box<dyn Externalizer>>,
) -> CompressionExternalizer {
let ct = options
.get("compression_type")
.and_then(|s| s.parse::<CompressionType>().ok())
.unwrap_or_default();
let level = options
.get("level")
.and_then(|s| s.parse::<u32>().ok())
.map(|l| flate2::Compression::new(l.clamp(0, 9)))
.unwrap_or_default();
CompressionExternalizer {
compression_type: ct,
level,
next,
}
}
#[cfg(feature = "compression")]
fn compress(bytes: &[u8], level: flate2::Compression, ct: &CompressionType) -> CalResult<Vec<u8>> {
use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder};
macro_rules! enc {
($T:ident) => {{
let mut e = $T::new(Vec::new(), level);
e.write_all(bytes)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))?;
e.finish()
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))
}};
}
match ct {
CompressionType::Gzip => enc!(GzEncoder),
CompressionType::Deflate => enc!(DeflateEncoder),
CompressionType::Zlib => enc!(ZlibEncoder),
}
}
#[cfg(feature = "compression")]
fn decompress(bytes: &[u8], ct: &CompressionType) -> CalResult<Vec<u8>> {
use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder};
macro_rules! dec {
($T:ident) => {{
let mut d = $T::new(bytes);
let mut out = Vec::new();
d.read_to_end(&mut out)
.map_err(|e| CalError::new(CalErrorKind::SerializationError, e.to_string()))?;
Ok(out)
}};
}
match ct {
CompressionType::Gzip => dec!(GzDecoder),
CompressionType::Deflate => dec!(DeflateDecoder),
CompressionType::Zlib => dec!(ZlibDecoder),
}
}
#[cfg(feature = "compression")]
pub fn new_gzip_externalizer(next: Box<dyn Externalizer>) -> CompressionExternalizer {
CompressionExternalizer::new(
Some(next),
CompressionType::Gzip,
flate2::Compression::default(),
)
}
pub struct ExternalizerBuilder {
name: String,
kind: Option<String>,
options: HashMap<String, String>,
next: Option<Box<ExternalizerBuilder>>,
}
impl Default for ExternalizerBuilder {
fn default() -> Self {
Self::new()
}
}
impl ExternalizerBuilder {
pub fn new() -> Self {
Self {
name: String::new(),
kind: None,
options: HashMap::new(),
next: None,
}
}
pub fn kind(mut self, kind: &str) -> Self {
self.kind = Some(kind.to_string());
self
}
pub fn option<T: ToString>(mut self, key: &str, value: T) -> Self {
self.options.insert(key.to_string(), value.to_string());
self
}
pub fn chain(self, outer_name: &str) -> Self {
ExternalizerBuilder {
name: outer_name.to_string(),
kind: None,
options: HashMap::new(),
next: Some(Box::new(self)),
}
}
pub fn build(self) -> CalResult<Box<dyn Externalizer>> {
let ExternalizerBuilder {
name,
kind,
options,
next,
} = self;
let effective_kind = kind.as_deref().unwrap_or(&name).to_string();
match next {
None => match effective_kind.as_str() {
"xml" => Ok(Box::new(xml_from_options(&options))),
other => Err(CalError::new(
CalErrorKind::SerializationError,
format!("unknown leaf externalizer type: '{other}'"),
)),
},
Some(inner_b) => {
#[cfg(feature = "compression")]
if effective_kind == "compression" {
let inner_ext = inner_b.build()?;
return Ok(Box::new(compression_from_options(
&options,
Some(inner_ext),
)));
}
drop(inner_b);
Err(CalError::new(
CalErrorKind::SerializationError,
format!("unknown chain externalizer type: '{effective_kind}'"),
))
}
}
}
}
pub fn builder(name: impl Into<String>) -> ExternalizerBuilder {
ExternalizerBuilder {
name: name.into(),
kind: None,
options: HashMap::new(),
next: None,
}
}
pub fn build_externalizer(name: &str, config: &CalConfig) -> CalResult<Box<dyn Externalizer>> {
match config.externalizer.get(name) {
Some(ext_cfg) => build_from_config(ext_cfg, config),
None => build_builtin(name),
}
}
fn build_from_config(
ext_cfg: &ExternalizerConfig,
_config: &CalConfig,
) -> CalResult<Box<dyn Externalizer>> {
match ext_cfg {
ExternalizerConfig::Xml { pretty } => {
let format = if *pretty {
SerializationFormat::PrettyXml
} else {
SerializationFormat::Xml
};
Ok(Box::new(XmlExternalizer { format, next: None }))
}
#[cfg(feature = "compression")]
ExternalizerConfig::Compression {
inner,
compression_type,
options,
} => {
let inner_ext = build_externalizer(inner, _config)?;
let level = options
.get("level")
.and_then(|v| v.as_integer())
.map(|l| flate2::Compression::new(l.clamp(0, 9) as u32))
.unwrap_or_default();
Ok(Box::new(CompressionExternalizer {
compression_type: compression_type.clone(),
level,
next: Some(inner_ext),
}))
}
}
}
fn build_builtin(name: &str) -> CalResult<Box<dyn Externalizer>> {
match name {
"xml" => Ok(Box::new(XmlExternalizer {
format: SerializationFormat::Xml,
next: None,
})),
#[cfg(feature = "compression")]
"compression" | "gzip" => {
let xml = build_builtin("xml")?;
Ok(Box::new(CompressionExternalizer {
compression_type: CompressionType::Gzip,
level: flate2::Compression::default(),
next: Some(xml),
}))
}
other => Err(CalError::new(
CalErrorKind::SerializationError,
format!("unknown externalizer: '{other}'"),
)),
}
}
#[derive(Default)]
pub struct XmlExternalizerLoader;
impl ExternalizerLoader for XmlExternalizerLoader {
fn get_externalizer(
&self,
encoding: &str,
_schema_version: &str,
_vendor_version: &str,
) -> CalResult<Box<dyn Externalizer>> {
match encoding {
XML_EXTERNALIZER_ENCODING => Ok(Box::new(XmlExternalizer {
format: SerializationFormat::default(),
next: None,
})),
other => Err(CalError::new(
CalErrorKind::SerializationError,
format!("unsupported externalizer encoding: '{other}'"),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct TestMsg {
value: i32,
}
impl CalMessage for TestMsg {
fn message_type_name() -> crate::QName {
crate::QName::new(None, "TestMsg")
}
fn cal_create() -> Self {
TestMsg { value: 0 }
}
}
const ROOT: &str = "TestMsg";
#[test]
fn xml_round_trip_str() {
let ext = XmlExternalizer::new(SerializationFormat::Xml);
let msg = TestMsg { value: 42 };
let s = write_to_string(&ext, &msg, ROOT).unwrap();
let decoded: TestMsg = read_from_str(&ext, &s).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn xml_round_trip_bytes() {
let ext = XmlExternalizer::new(SerializationFormat::Xml);
let msg = TestMsg { value: 7 };
let bytes = write_to_bytes(&ext, &msg, ROOT).unwrap();
let decoded: TestMsg = read_from_bytes(&ext, &bytes).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn xml_round_trip_reader_writer() {
let ext = XmlExternalizer::new(SerializationFormat::Xml);
let msg = TestMsg { value: 99 };
let mut buf = Vec::new();
write_to_writer(&ext, &msg, ROOT, &mut buf).unwrap();
let decoded: TestMsg = read_from_reader(&ext, &mut buf.as_slice()).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn xml_externalizer_identity() {
let ext: Box<dyn Externalizer> = Box::new(XmlExternalizer::new(SerializationFormat::Xml));
assert_eq!(ext.get_encoding(), "xml");
assert_eq!(ext.get_cal_api_version(), "2.5");
assert_eq!(ext.get_vendor(), "rcal");
assert!(!ext.message_read_only());
assert!(!ext.message_write_only());
assert!(ext.supports_object_read());
assert!(ext.supports_object_write());
}
#[test]
fn loader_ok() {
let loader = XmlExternalizerLoader::default();
let ext = loader.get_externalizer("xml", "2.5", "1.0").unwrap();
assert_eq!(ext.get_encoding(), "xml");
}
#[test]
fn loader_unknown_encoding() {
let loader = XmlExternalizerLoader::default();
let result = loader.get_externalizer("binary", "2.5", "1.0");
assert_eq!(
result.err().unwrap().kind(),
&CalErrorKind::SerializationError
);
}
#[cfg(feature = "compression")]
#[test]
fn compression_round_trip() {
let xml: Box<dyn Externalizer> = Box::new(XmlExternalizer::new(SerializationFormat::Xml));
let gzip = new_gzip_externalizer(xml);
let msg = TestMsg { value: 55 };
let compressed = write_to_bytes(&gzip, &msg, ROOT).unwrap();
let decoded: TestMsg = read_from_bytes(&gzip, &compressed).unwrap();
assert_eq!(decoded, msg);
}
#[cfg(feature = "compression")]
#[test]
fn builder_xml_gzip() {
let ext = builder("xml")
.kind("xml")
.chain("gzip")
.kind("compression")
.build()
.unwrap();
let msg = TestMsg { value: 99 };
let compressed = write_to_bytes(ext.as_ref(), &msg, ROOT).unwrap();
let decoded: TestMsg = read_from_bytes(ext.as_ref(), &compressed).unwrap();
assert_eq!(decoded, msg);
assert_eq!(ext.get_encoding(), "gzip");
}
}