Initial Rust WSON serde implementation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+75
@@ -0,0 +1,75 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wson"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "wson"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "WSON text and JBON binary serialization with serde support"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -0,0 +1,174 @@
|
||||
use super::parser::marker;
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{Graph, Node, NodeId};
|
||||
use crate::leb128;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
|
||||
fn count_occurrences(graph: &Graph) -> HashMap<NodeId, usize> {
|
||||
fn visit(graph: &Graph, id: NodeId, counts: &mut HashMap<NodeId, usize>) {
|
||||
let count = counts.entry(id).or_insert(0);
|
||||
*count += 1;
|
||||
if *count != 1 {
|
||||
return;
|
||||
}
|
||||
match graph.node(id) {
|
||||
Node::Array(items) => items.iter().for_each(|child| visit(graph, *child, counts)),
|
||||
Node::Object(entries) => entries
|
||||
.values()
|
||||
.for_each(|child| visit(graph, *child, counts)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut counts = HashMap::new();
|
||||
visit(graph, graph.root(), &mut counts);
|
||||
counts
|
||||
}
|
||||
|
||||
fn assign_ids(graph: &Graph, counts: &HashMap<NodeId, usize>) -> HashMap<NodeId, usize> {
|
||||
fn visit(
|
||||
graph: &Graph,
|
||||
id: NodeId,
|
||||
counts: &HashMap<NodeId, usize>,
|
||||
seen: &mut HashSet<NodeId>,
|
||||
ids: &mut HashMap<NodeId, usize>,
|
||||
) {
|
||||
if counts.get(&id).copied().unwrap_or(0) > 1 && !ids.contains_key(&id) {
|
||||
let next = ids.len();
|
||||
ids.insert(id, next);
|
||||
}
|
||||
if !seen.insert(id) {
|
||||
return;
|
||||
}
|
||||
match graph.node(id) {
|
||||
Node::Array(items) => items
|
||||
.iter()
|
||||
.for_each(|child| visit(graph, *child, counts, seen, ids)),
|
||||
Node::Object(entries) => entries
|
||||
.values()
|
||||
.for_each(|child| visit(graph, *child, counts, seen, ids)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut ids = HashMap::new();
|
||||
visit(graph, graph.root(), counts, &mut HashSet::new(), &mut ids);
|
||||
ids
|
||||
}
|
||||
|
||||
struct DumpState<'a> {
|
||||
graph: &'a Graph,
|
||||
cfg: &'a Config,
|
||||
ids: HashMap<NodeId, usize>,
|
||||
dumped: HashSet<NodeId>,
|
||||
visiting: HashSet<NodeId>,
|
||||
}
|
||||
|
||||
impl<'a> DumpState<'a> {
|
||||
fn new(graph: &'a Graph, cfg: &'a Config) -> Self {
|
||||
let ids = if cfg.serialize_references {
|
||||
let counts = count_occurrences(graph);
|
||||
assign_ids(graph, &counts)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
Self {
|
||||
graph,
|
||||
cfg,
|
||||
ids,
|
||||
dumped: HashSet::new(),
|
||||
visiting: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_node<W: Write>(&mut self, id: NodeId, out: &mut W) -> Result<()> {
|
||||
if self.cfg.serialize_references {
|
||||
if let Some(reference_id) = self.ids.get(&id).copied() {
|
||||
if !self.dumped.insert(id) {
|
||||
out.write_all(&[marker::REFERENCE])?;
|
||||
leb128::encode(&mut *out, reference_id as i64)?;
|
||||
return Ok(());
|
||||
}
|
||||
out.write_all(&[marker::ID])?;
|
||||
leb128::encode(&mut *out, reference_id as i64)?;
|
||||
}
|
||||
}
|
||||
|
||||
match self.graph.node(id) {
|
||||
Node::Null => out.write_all(&[marker::NULL])?,
|
||||
Node::Bool(true) => out.write_all(&[marker::TRUE])?,
|
||||
Node::Bool(false) => out.write_all(&[marker::FALSE])?,
|
||||
Node::Integer(value) => {
|
||||
out.write_all(&[marker::INT])?;
|
||||
leb128::encode(&mut *out, *value)?;
|
||||
}
|
||||
Node::Float(value) => {
|
||||
out.write_all(&[marker::FLOAT])?;
|
||||
leb128::encode_double(&mut *out, *value)?;
|
||||
}
|
||||
Node::String(value) => {
|
||||
let bytes = value.as_bytes();
|
||||
if bytes.is_empty() {
|
||||
out.write_all(&[marker::EMPTY_STRING])?;
|
||||
} else if bytes.len() < (marker::LARGE_STRING - marker::EMPTY_STRING) as usize {
|
||||
out.write_all(&[marker::EMPTY_STRING + bytes.len() as u8])?;
|
||||
out.write_all(bytes)?;
|
||||
} else {
|
||||
out.write_all(&[marker::LARGE_STRING])?;
|
||||
leb128::encode(&mut *out, bytes.len() as i64)?;
|
||||
out.write_all(bytes)?;
|
||||
}
|
||||
}
|
||||
Node::Array(items) => {
|
||||
if !self.cfg.serialize_references && !self.visiting.insert(id) {
|
||||
return Err(Error::Cycle);
|
||||
}
|
||||
if items.is_empty() {
|
||||
out.write_all(&[marker::EMPTY_ARRAY])?;
|
||||
} else if items.len() < (marker::LARGE_ARRAY - marker::EMPTY_ARRAY) as usize {
|
||||
out.write_all(&[marker::EMPTY_ARRAY + items.len() as u8])?;
|
||||
} else {
|
||||
out.write_all(&[marker::LARGE_ARRAY])?;
|
||||
leb128::encode(&mut *out, items.len() as i64)?;
|
||||
}
|
||||
for child in items {
|
||||
self.dump_node(*child, out)?;
|
||||
}
|
||||
self.visiting.remove(&id);
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
if !self.cfg.serialize_references && !self.visiting.insert(id) {
|
||||
return Err(Error::Cycle);
|
||||
}
|
||||
if entries.is_empty() {
|
||||
out.write_all(&[marker::EMPTY_OBJECT])?;
|
||||
} else if entries.len() < (marker::LARGE_OBJECT - marker::EMPTY_OBJECT) as usize {
|
||||
out.write_all(&[marker::EMPTY_OBJECT + entries.len() as u8])?;
|
||||
} else {
|
||||
out.write_all(&[marker::LARGE_OBJECT])?;
|
||||
leb128::encode(&mut *out, entries.len() as i64)?;
|
||||
}
|
||||
for (key, child) in entries {
|
||||
leb128::encode(&mut *out, key.len() as i64)?;
|
||||
out.write_all(key.as_bytes())?;
|
||||
self.dump_node(*child, out)?;
|
||||
}
|
||||
self.visiting.remove(&id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump_graph(graph: &Graph, cfg: &Config) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::new();
|
||||
DumpState::new(graph, cfg).dump_node(graph.root(), &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn dump_graph_to_writer<W: Write>(graph: &Graph, cfg: &Config, mut writer: W) -> Result<()> {
|
||||
let bytes = dump_graph(graph, cfg)?;
|
||||
writer.write_all(&bytes)?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
pub mod dumper;
|
||||
pub mod parser;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::de::ValueDeserializer;
|
||||
use crate::error::Result;
|
||||
use crate::graph::Graph;
|
||||
use crate::ser::ValueSerializer;
|
||||
use crate::value::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub fn parse_slice(input: &[u8], cfg: &Config) -> Result<Graph> {
|
||||
parser::parse_slice(input, cfg)
|
||||
}
|
||||
|
||||
pub fn parse_reader<R: Read>(reader: R, cfg: &Config) -> Result<Graph> {
|
||||
parser::parse_reader(reader, cfg)
|
||||
}
|
||||
|
||||
pub fn dump_graph(graph: &Graph, cfg: &Config) -> Result<Vec<u8>> {
|
||||
dumper::dump_graph(graph, cfg)
|
||||
}
|
||||
|
||||
pub fn dump_graph_to_writer<W: Write>(graph: &Graph, cfg: &Config, writer: W) -> Result<()> {
|
||||
dumper::dump_graph_to_writer(graph, cfg, writer)
|
||||
}
|
||||
|
||||
pub fn to_vec<T: ?Sized + Serialize>(value: &T) -> Result<Vec<u8>> {
|
||||
to_vec_with_config(value, &Config::default())
|
||||
}
|
||||
|
||||
pub fn to_vec_with_config<T: ?Sized + Serialize>(value: &T, cfg: &Config) -> Result<Vec<u8>> {
|
||||
let value = value.serialize(ValueSerializer)?;
|
||||
let graph = Graph::from_value(&value);
|
||||
dumper::dump_graph(&graph, cfg)
|
||||
}
|
||||
|
||||
pub fn to_writer<T: ?Sized + Serialize, W: Write>(value: &T, writer: W) -> Result<()> {
|
||||
to_writer_with_config(value, &Config::default(), writer)
|
||||
}
|
||||
|
||||
pub fn to_writer_with_config<T: ?Sized + Serialize, W: Write>(
|
||||
value: &T,
|
||||
cfg: &Config,
|
||||
writer: W,
|
||||
) -> Result<()> {
|
||||
let value = value.serialize(ValueSerializer)?;
|
||||
let graph = Graph::from_value(&value);
|
||||
dumper::dump_graph_to_writer(&graph, cfg, writer)
|
||||
}
|
||||
|
||||
pub fn from_slice<'a, T: Deserialize<'a>>(input: &'a [u8]) -> Result<T> {
|
||||
from_slice_with_config(input, &Config::default())
|
||||
}
|
||||
|
||||
pub fn from_slice_with_config<'a, T: Deserialize<'a>>(input: &'a [u8], cfg: &Config) -> Result<T> {
|
||||
let graph = parser::parse_slice(input, cfg)?;
|
||||
let value = Value::from_graph(&graph)?;
|
||||
T::deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
|
||||
pub fn from_reader<'a, T: Deserialize<'a>, R: Read>(reader: R) -> Result<T> {
|
||||
from_reader_with_config(reader, &Config::default())
|
||||
}
|
||||
|
||||
pub fn from_reader_with_config<'a, T: Deserialize<'a>, R: Read>(
|
||||
reader: R,
|
||||
cfg: &Config,
|
||||
) -> Result<T> {
|
||||
let graph = parser::parse_reader(reader, cfg)?;
|
||||
let value = Value::from_graph(&graph)?;
|
||||
T::deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{ContainerKind, Graph, Node, ParseStack};
|
||||
use crate::leb128;
|
||||
use std::io::Read;
|
||||
|
||||
pub(crate) mod marker {
|
||||
pub const FLOAT: u8 = 0x00;
|
||||
pub const INT: u8 = 0x01;
|
||||
pub const NULL: u8 = 0x02;
|
||||
pub const TRUE: u8 = 0x03;
|
||||
pub const FALSE: u8 = 0x04;
|
||||
pub const REFERENCE: u8 = 0x05;
|
||||
pub const ID: u8 = 0x06;
|
||||
pub const EMPTY_STRING: u8 = 0x0d;
|
||||
pub const LARGE_STRING: u8 = 0x5d;
|
||||
pub const EMPTY_OBJECT: u8 = 0x5e;
|
||||
pub const LARGE_OBJECT: u8 = 0xae;
|
||||
pub const EMPTY_ARRAY: u8 = 0xaf;
|
||||
pub const LARGE_ARRAY: u8 = 0xff;
|
||||
}
|
||||
|
||||
struct ByteParser<'a> {
|
||||
bytes: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> ByteParser<'a> {
|
||||
fn new(bytes: &'a [u8]) -> Self {
|
||||
Self { bytes, pos: 0 }
|
||||
}
|
||||
|
||||
fn read_byte(&mut self) -> Option<u8> {
|
||||
let byte = *self.bytes.get(self.pos)?;
|
||||
self.pos += 1;
|
||||
Some(byte)
|
||||
}
|
||||
|
||||
fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
|
||||
if self.pos + len > self.bytes.len() {
|
||||
return Err(Error::binary(self.pos, "Unexpected end of file"));
|
||||
}
|
||||
let start = self.pos;
|
||||
self.pos += len;
|
||||
Ok(&self.bytes[start..start + len])
|
||||
}
|
||||
|
||||
fn decode(&mut self) -> Result<i64> {
|
||||
let mut decoder = leb128::Decoder::new(&self.bytes[self.pos..]);
|
||||
let value = decoder
|
||||
.decode()
|
||||
.map_err(|err| Error::binary(self.pos, err.to_string()))?;
|
||||
self.pos += decoder.bytes_read();
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn decode_double(&mut self) -> Result<f64> {
|
||||
let mut decoder = leb128::Decoder::new(&self.bytes[self.pos..]);
|
||||
let value = decoder
|
||||
.decode_double()
|
||||
.map_err(|err| Error::binary(self.pos, err.to_string()))?;
|
||||
self.pos += decoder.bytes_read();
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn decode_len(&mut self) -> Result<usize> {
|
||||
let len = self.decode()?;
|
||||
usize::try_from(len).map_err(|_| Error::binary(self.pos, "Negative size"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_slice(input: &[u8], cfg: &Config) -> Result<Graph> {
|
||||
let mut stream = ByteParser::new(input);
|
||||
let mut stack = ParseStack::new(cfg);
|
||||
let mut current_id: Option<i64> = None;
|
||||
|
||||
loop {
|
||||
if stack.expecting_object_key() {
|
||||
let key_len = stream.decode_len()?;
|
||||
let key_bytes = stream.read_exact(key_len)?;
|
||||
let key = String::from_utf8(key_bytes.to_vec())
|
||||
.map_err(|err| Error::binary(stream.pos, err.to_string()))?;
|
||||
stack.object_key(key);
|
||||
}
|
||||
|
||||
let Some(marker_byte) = stream.read_byte() else {
|
||||
break;
|
||||
};
|
||||
let position = stream.pos - 1;
|
||||
|
||||
if marker_byte == marker::ID {
|
||||
if stack.id_map.is_none() {
|
||||
return Err(Error::binary(position, "Illegal byte"));
|
||||
}
|
||||
current_id = Some(stream.decode()?);
|
||||
} else if marker_byte == marker::REFERENCE {
|
||||
if stack.id_map.is_none() {
|
||||
return Err(Error::binary(position, "Illegal byte"));
|
||||
}
|
||||
let id = stream.decode()?;
|
||||
let node = stack
|
||||
.resolve_reference(id)
|
||||
.map_err(|err| Error::binary(position, err.to_string()))?;
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::NULL {
|
||||
let node = stack.builder.add(Node::Null);
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::TRUE {
|
||||
let node = stack.builder.add(Node::Bool(true));
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::FALSE {
|
||||
let node = stack.builder.add(Node::Bool(false));
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::INT {
|
||||
let value = stream.decode()?;
|
||||
let node = stack.builder.add(Node::Integer(value));
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::FLOAT {
|
||||
let value = stream.decode_double()?;
|
||||
let node = stack.builder.add(Node::Float(value));
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::EMPTY_STRING {
|
||||
let node = stack.builder.add(Node::String(String::new()));
|
||||
stack.add_value(node);
|
||||
} else if marker_byte > marker::EMPTY_STRING && marker_byte < marker::LARGE_STRING {
|
||||
let len = (marker_byte - marker::EMPTY_STRING) as usize;
|
||||
let bytes = stream.read_exact(len)?;
|
||||
let text = String::from_utf8(bytes.to_vec())
|
||||
.map_err(|err| Error::binary(stream.pos, err.to_string()))?;
|
||||
let node = stack.builder.add(Node::String(text));
|
||||
stack.add_value(node);
|
||||
} else if marker_byte == marker::LARGE_STRING {
|
||||
let len = stream.decode_len()?;
|
||||
let bytes = stream.read_exact(len)?;
|
||||
let text = String::from_utf8(bytes.to_vec())
|
||||
.map_err(|err| Error::binary(stream.pos, err.to_string()))?;
|
||||
let node = stack.builder.add(Node::String(text));
|
||||
stack.add_value(node);
|
||||
} else if (marker::EMPTY_ARRAY..=marker::LARGE_ARRAY).contains(&marker_byte) {
|
||||
let size = if marker_byte == marker::LARGE_ARRAY {
|
||||
Some(stream.decode_len()?)
|
||||
} else {
|
||||
Some((marker_byte - marker::EMPTY_ARRAY) as usize)
|
||||
};
|
||||
let node = stack.begin_array(size)?;
|
||||
if let Some(id) = current_id.take() {
|
||||
stack.register_id(id, node);
|
||||
}
|
||||
} else if (marker::EMPTY_OBJECT..=marker::LARGE_OBJECT).contains(&marker_byte) {
|
||||
let size = if marker_byte == marker::LARGE_OBJECT {
|
||||
Some(stream.decode_len()?)
|
||||
} else {
|
||||
Some((marker_byte - marker::EMPTY_OBJECT) as usize)
|
||||
};
|
||||
let node = stack.begin_object(size)?;
|
||||
if let Some(id) = current_id.take() {
|
||||
stack.register_id(id, node);
|
||||
}
|
||||
} else {
|
||||
return Err(Error::binary(
|
||||
position,
|
||||
format!("Illegal byte at position {position}: 0x{marker_byte:02x}"),
|
||||
));
|
||||
}
|
||||
stack.auto_close();
|
||||
}
|
||||
|
||||
if stack.depth() > 1 {
|
||||
let kind = match stack.top().kind {
|
||||
ContainerKind::Array => "array",
|
||||
ContainerKind::Object => "object",
|
||||
};
|
||||
return Err(Error::binary(stream.pos, format!("Unfinished {kind}")));
|
||||
}
|
||||
stack.finish()
|
||||
}
|
||||
|
||||
pub fn parse_reader<R: Read>(mut reader: R, cfg: &Config) -> Result<Graph> {
|
||||
let mut input = Vec::new();
|
||||
reader.read_to_end(&mut input)?;
|
||||
parse_slice(&input, cfg)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
/// Maximum container nesting depth accepted by parsers.
|
||||
pub max_depth: usize,
|
||||
/// Enable WSON reference ids/references (`(id)` / `$id`, binary `0x06` / `0x05`).
|
||||
pub serialize_references: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_depth: 1_048_576,
|
||||
serialize_references: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::value::Value;
|
||||
use serde::de::{self, EnumAccess, IntoDeserializer, MapAccess, SeqAccess, VariantAccess, Visitor};
|
||||
use serde::Deserializer as _;
|
||||
use std::collections::BTreeMap;
|
||||
use std::vec;
|
||||
|
||||
pub struct ValueDeserializer {
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl ValueDeserializer {
|
||||
pub fn new(value: Value) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_type(expected: &str, value: &Value) -> Error {
|
||||
Error::Message(format!(
|
||||
"invalid type: {:?}, expected {expected}",
|
||||
value.ty()
|
||||
))
|
||||
}
|
||||
|
||||
fn integer(value: Value, expected: &str) -> Result<i64> {
|
||||
match value {
|
||||
Value::Integer(v) => Ok(v),
|
||||
other => Err(invalid_type(expected, &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn float(value: Value, expected: &str) -> Result<f64> {
|
||||
match value {
|
||||
Value::Float(v) => Ok(v),
|
||||
other => Err(invalid_type(expected, &other)),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! deserialize_signed {
|
||||
($method:ident, $visit:ident, $ty:ty) => {
|
||||
fn $method<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let value = integer(self.value, stringify!($ty))?;
|
||||
let value = <$ty>::try_from(value).map_err(|_| {
|
||||
Error::Message(format!("integer out of range for {}", stringify!($ty)))
|
||||
})?;
|
||||
visitor.$visit(value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! deserialize_unsigned {
|
||||
($method:ident, $visit:ident, $ty:ty) => {
|
||||
fn $method<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let value = integer(self.value, stringify!($ty))?;
|
||||
let value = <$ty>::try_from(value).map_err(|_| {
|
||||
Error::Message(format!("integer out of range for {}", stringify!($ty)))
|
||||
})?;
|
||||
visitor.$visit(value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<'de> de::Deserializer<'de> for ValueDeserializer {
|
||||
type Error = Error;
|
||||
|
||||
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::Null => visitor.visit_unit(),
|
||||
Value::Bool(v) => visitor.visit_bool(v),
|
||||
Value::Integer(v) => visitor.visit_i64(v),
|
||||
Value::Float(v) => visitor.visit_f64(v),
|
||||
Value::String(v) => visitor.visit_string(v),
|
||||
Value::Array(v) => visitor.visit_seq(SeqDeserializer {
|
||||
iter: v.into_iter(),
|
||||
}),
|
||||
Value::Object(v) => visitor.visit_map(MapDeserializer {
|
||||
iter: v.into_iter(),
|
||||
value: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
deserialize_signed!(deserialize_i8, visit_i8, i8);
|
||||
deserialize_signed!(deserialize_i16, visit_i16, i16);
|
||||
deserialize_signed!(deserialize_i32, visit_i32, i32);
|
||||
|
||||
fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_i64(integer(self.value, "i64")?)
|
||||
}
|
||||
|
||||
deserialize_unsigned!(deserialize_u8, visit_u8, u8);
|
||||
deserialize_unsigned!(deserialize_u16, visit_u16, u16);
|
||||
deserialize_unsigned!(deserialize_u32, visit_u32, u32);
|
||||
deserialize_unsigned!(deserialize_u64, visit_u64, u64);
|
||||
|
||||
fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_f32(float(self.value, "f32")? as f32)
|
||||
}
|
||||
|
||||
fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_f64(float(self.value, "f64")?)
|
||||
}
|
||||
|
||||
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::Bool(v) => visitor.visit_bool(v),
|
||||
other => Err(invalid_type("bool", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_char<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::String(v) => {
|
||||
let mut chars = v.chars();
|
||||
match (chars.next(), chars.next()) {
|
||||
(Some(c), None) => visitor.visit_char(c),
|
||||
_ => Err(Error::Message("invalid char".to_string())),
|
||||
}
|
||||
}
|
||||
other => Err(invalid_type("char", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_string(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::String(v) => visitor.visit_string(v),
|
||||
other => Err(invalid_type("string", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
Err(Error::Unsupported("bytes"))
|
||||
}
|
||||
|
||||
fn deserialize_byte_buf<V>(self, _visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
Err(Error::Unsupported("bytes"))
|
||||
}
|
||||
|
||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::Null => visitor.visit_none(),
|
||||
other => visitor.visit_some(ValueDeserializer::new(other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::Null => visitor.visit_unit(),
|
||||
other => Err(invalid_type("unit", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_unit_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_unit(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_newtype_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_newtype_struct(self)
|
||||
}
|
||||
|
||||
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::Array(v) => visitor.visit_seq(SeqDeserializer {
|
||||
iter: v.into_iter(),
|
||||
}),
|
||||
other => Err(invalid_type("array", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_seq(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_tuple_struct<V>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
visitor: V,
|
||||
) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_seq(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::Object(v) => visitor.visit_map(MapDeserializer {
|
||||
iter: v.into_iter(),
|
||||
value: None,
|
||||
}),
|
||||
other => Err(invalid_type("object", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_struct<V>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_fields: &'static [&'static str],
|
||||
visitor: V,
|
||||
) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_map(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_enum<V>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variants: &'static [&'static str],
|
||||
visitor: V,
|
||||
) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Value::String(variant) => visitor.visit_enum(EnumDeserializer {
|
||||
variant,
|
||||
value: None,
|
||||
}),
|
||||
Value::Object(mut object) => {
|
||||
if object.len() != 1 {
|
||||
return Err(Error::Message(
|
||||
"enum object must have exactly one entry".to_string(),
|
||||
));
|
||||
}
|
||||
let (variant, value) = object.pop_first().expect("one enum entry");
|
||||
visitor.visit_enum(EnumDeserializer {
|
||||
variant,
|
||||
value: Some(value),
|
||||
})
|
||||
}
|
||||
other => Err(invalid_type("enum", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_string(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_any(visitor)
|
||||
}
|
||||
|
||||
fn is_human_readable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct SeqDeserializer {
|
||||
iter: vec::IntoIter<Value>,
|
||||
}
|
||||
|
||||
impl<'de> SeqAccess<'de> for SeqDeserializer {
|
||||
type Error = Error;
|
||||
|
||||
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
|
||||
where
|
||||
T: de::DeserializeSeed<'de>,
|
||||
{
|
||||
match self.iter.next() {
|
||||
Some(value) => seed.deserialize(ValueDeserializer::new(value)).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Option<usize> {
|
||||
Some(self.iter.len())
|
||||
}
|
||||
}
|
||||
|
||||
struct MapDeserializer {
|
||||
iter: <BTreeMap<String, Value> as IntoIterator>::IntoIter,
|
||||
value: Option<Value>,
|
||||
}
|
||||
|
||||
impl<'de> MapAccess<'de> for MapDeserializer {
|
||||
type Error = Error;
|
||||
|
||||
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
|
||||
where
|
||||
K: de::DeserializeSeed<'de>,
|
||||
{
|
||||
match self.iter.next() {
|
||||
Some((key, value)) => {
|
||||
self.value = Some(value);
|
||||
let key_deserializer: de::value::StringDeserializer<Error> =
|
||||
key.into_deserializer();
|
||||
seed.deserialize(key_deserializer).map(Some)
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
|
||||
where
|
||||
V: de::DeserializeSeed<'de>,
|
||||
{
|
||||
let value = self
|
||||
.value
|
||||
.take()
|
||||
.ok_or_else(|| Error::Message("next_value called before next_key".to_string()))?;
|
||||
seed.deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Option<usize> {
|
||||
Some(self.iter.len())
|
||||
}
|
||||
}
|
||||
|
||||
struct EnumDeserializer {
|
||||
variant: String,
|
||||
value: Option<Value>,
|
||||
}
|
||||
|
||||
impl<'de> EnumAccess<'de> for EnumDeserializer {
|
||||
type Error = Error;
|
||||
type Variant = VariantDeserializer;
|
||||
|
||||
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant)>
|
||||
where
|
||||
V: de::DeserializeSeed<'de>,
|
||||
{
|
||||
let variant_deserializer: de::value::StringDeserializer<Error> =
|
||||
self.variant.into_deserializer();
|
||||
let variant = seed.deserialize(variant_deserializer)?;
|
||||
Ok((variant, VariantDeserializer { value: self.value }))
|
||||
}
|
||||
}
|
||||
|
||||
struct VariantDeserializer {
|
||||
value: Option<Value>,
|
||||
}
|
||||
|
||||
impl<'de> VariantAccess<'de> for VariantDeserializer {
|
||||
type Error = Error;
|
||||
|
||||
fn unit_variant(self) -> Result<()> {
|
||||
match self.value {
|
||||
None | Some(Value::Null) => Ok(()),
|
||||
Some(other) => Err(invalid_type("unit variant", &other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value>
|
||||
where
|
||||
T: de::DeserializeSeed<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Some(value) => seed.deserialize(ValueDeserializer::new(value)),
|
||||
None => Err(Error::Message("missing newtype variant value".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Some(Value::Array(values)) => {
|
||||
ValueDeserializer::new(Value::Array(values)).deserialize_seq(visitor)
|
||||
}
|
||||
Some(other) => Err(invalid_type("tuple variant", &other)),
|
||||
None => Err(Error::Message("missing tuple variant value".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_variant<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match self.value {
|
||||
Some(Value::Object(entries)) => {
|
||||
ValueDeserializer::new(Value::Object(entries)).deserialize_map(visitor)
|
||||
}
|
||||
Some(other) => Err(invalid_type("struct variant", &other)),
|
||||
None => Err(Error::Message("missing struct variant value".to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
use serde::{de, ser};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Message(String),
|
||||
Io(std::io::Error),
|
||||
Parse {
|
||||
line: Option<usize>,
|
||||
column: Option<usize>,
|
||||
position: Option<usize>,
|
||||
message: String,
|
||||
},
|
||||
MaxDepthExceeded {
|
||||
max_depth: usize,
|
||||
},
|
||||
Type(String),
|
||||
Unsupported(&'static str),
|
||||
Cycle,
|
||||
TrailingCharacters,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn text(line: usize, column: usize, message: impl Into<String>) -> Self {
|
||||
Self::Parse {
|
||||
line: Some(line),
|
||||
column: Some(column),
|
||||
position: None,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binary(position: usize, message: impl Into<String>) -> Self {
|
||||
Self::Parse {
|
||||
line: None,
|
||||
column: None,
|
||||
position: Some(position),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::Error for Error {
|
||||
fn custom<T: Display>(msg: T) -> Self {
|
||||
Error::Message(msg.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl de::Error for Error {
|
||||
fn custom<T: Display>(msg: T) -> Self {
|
||||
Error::Message(msg.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Message(msg) => f.write_str(msg),
|
||||
Error::Io(err) => write!(f, "I/O error: {err}"),
|
||||
Error::Parse {
|
||||
line: Some(line),
|
||||
column: Some(column),
|
||||
message,
|
||||
..
|
||||
} => write!(f, "Error at line {line} column {column}: {message}"),
|
||||
Error::Parse {
|
||||
position: Some(position),
|
||||
message,
|
||||
..
|
||||
} => write!(f, "Error at position {position}: {message}"),
|
||||
Error::Parse { message, .. } => f.write_str(message),
|
||||
Error::MaxDepthExceeded { max_depth } => {
|
||||
write!(f, "Object is too deep, max allowed depth is {max_depth}")
|
||||
}
|
||||
Error::Type(msg) => f.write_str(msg),
|
||||
Error::Unsupported(what) => write!(f, "unsupported: {what}"),
|
||||
Error::Cycle => f.write_str("cyclic value cannot be serialized without references"),
|
||||
Error::TrailingCharacters => f.write_str("trailing characters"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::Io(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Error::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(value: std::string::FromUtf8Error) -> Self {
|
||||
Error::Message(value.to_string())
|
||||
}
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct NodeId(pub usize);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Node {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
Array(Vec<NodeId>),
|
||||
Object(BTreeMap<String, NodeId>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Graph {
|
||||
nodes: Vec<Node>,
|
||||
root: NodeId,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn new(root: NodeId, nodes: Vec<Node>) -> Self {
|
||||
Self { nodes, root }
|
||||
}
|
||||
|
||||
pub fn root(&self) -> NodeId {
|
||||
self.root
|
||||
}
|
||||
|
||||
pub fn node(&self, id: NodeId) -> &Node {
|
||||
&self.nodes[id.0]
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.nodes.is_empty()
|
||||
}
|
||||
|
||||
pub fn from_value(value: &crate::value::Value) -> Self {
|
||||
fn build(builder: &mut GraphBuilder, value: &crate::value::Value) -> NodeId {
|
||||
match value {
|
||||
crate::value::Value::Null => builder.add(Node::Null),
|
||||
crate::value::Value::Bool(v) => builder.add(Node::Bool(*v)),
|
||||
crate::value::Value::Integer(v) => builder.add(Node::Integer(*v)),
|
||||
crate::value::Value::Float(v) => builder.add(Node::Float(*v)),
|
||||
crate::value::Value::String(v) => builder.add(Node::String(v.clone())),
|
||||
crate::value::Value::Array(items) => {
|
||||
let array = builder.add(Node::Array(Vec::new()));
|
||||
for item in items {
|
||||
let child = build(builder, item);
|
||||
builder.push_array_item(array, child);
|
||||
}
|
||||
array
|
||||
}
|
||||
crate::value::Value::Object(entries) => {
|
||||
let object = builder.add(Node::Object(BTreeMap::new()));
|
||||
for (key, item) in entries {
|
||||
let child = build(builder, item);
|
||||
builder.put_object_value(object, key.clone(), child);
|
||||
}
|
||||
object
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut builder = GraphBuilder::new();
|
||||
let root = build(&mut builder, value);
|
||||
builder.finish(root)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GraphBuilder {
|
||||
nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
impl GraphBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self { nodes: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add(&mut self, node: Node) -> NodeId {
|
||||
let id = NodeId(self.nodes.len());
|
||||
self.nodes.push(node);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_array_item(&mut self, array: NodeId, value: NodeId) {
|
||||
if let Node::Array(items) = &mut self.nodes[array.0] {
|
||||
items.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_object_value(&mut self, object: NodeId, key: String, value: NodeId) {
|
||||
if let Node::Object(entries) = &mut self.nodes[object.0] {
|
||||
entries.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn array_len(&self, array: NodeId) -> usize {
|
||||
match &self.nodes[array.0] {
|
||||
Node::Array(items) => items.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_len(&self, object: NodeId) -> usize {
|
||||
match &self.nodes[object.0] {
|
||||
Node::Object(entries) => entries.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self, root: NodeId) -> Graph {
|
||||
Graph::new(root, self.nodes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ContainerKind {
|
||||
Array,
|
||||
Object,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct StackLevel {
|
||||
pub kind: ContainerKind,
|
||||
pub node: NodeId,
|
||||
pub expected_size: Option<usize>,
|
||||
pub current_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ParseStack {
|
||||
pub builder: GraphBuilder,
|
||||
stack: Vec<StackLevel>,
|
||||
max_depth: usize,
|
||||
pub id_map: Option<HashMap<i64, NodeId>>,
|
||||
}
|
||||
|
||||
impl ParseStack {
|
||||
pub fn new(cfg: &Config) -> Self {
|
||||
let mut builder = GraphBuilder::new();
|
||||
let root = builder.add(Node::Array(Vec::new()));
|
||||
Self {
|
||||
builder,
|
||||
stack: vec![StackLevel {
|
||||
kind: ContainerKind::Array,
|
||||
node: root,
|
||||
expected_size: None,
|
||||
current_key: None,
|
||||
}],
|
||||
max_depth: cfg.max_depth,
|
||||
id_map: cfg.serialize_references.then(HashMap::new),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn depth(&self) -> usize {
|
||||
self.stack.len()
|
||||
}
|
||||
|
||||
pub fn top(&self) -> &StackLevel {
|
||||
self.stack.last().expect("root stack level")
|
||||
}
|
||||
|
||||
pub fn top_mut(&mut self) -> &mut StackLevel {
|
||||
self.stack.last_mut().expect("root stack level")
|
||||
}
|
||||
|
||||
pub fn begin_array(&mut self, expected_size: Option<usize>) -> Result<NodeId> {
|
||||
if self.stack.len() == self.max_depth {
|
||||
return Err(Error::MaxDepthExceeded {
|
||||
max_depth: self.max_depth,
|
||||
});
|
||||
}
|
||||
let node = self.builder.add(Node::Array(Vec::new()));
|
||||
self.stack.push(StackLevel {
|
||||
kind: ContainerKind::Array,
|
||||
node,
|
||||
expected_size,
|
||||
current_key: None,
|
||||
});
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub fn begin_object(&mut self, expected_size: Option<usize>) -> Result<NodeId> {
|
||||
if self.stack.len() == self.max_depth {
|
||||
return Err(Error::MaxDepthExceeded {
|
||||
max_depth: self.max_depth,
|
||||
});
|
||||
}
|
||||
let node = self.builder.add(Node::Object(BTreeMap::new()));
|
||||
self.stack.push(StackLevel {
|
||||
kind: ContainerKind::Object,
|
||||
node,
|
||||
expected_size,
|
||||
current_key: None,
|
||||
});
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub fn add_value(&mut self, value: NodeId) {
|
||||
let top = self.stack.last_mut().expect("root stack level");
|
||||
match top.kind {
|
||||
ContainerKind::Array => self.builder.push_array_item(top.node, value),
|
||||
ContainerKind::Object => {
|
||||
let key = top.current_key.take().unwrap_or_default();
|
||||
self.builder.put_object_value(top.node, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_key(&mut self, key: String) {
|
||||
self.top_mut().current_key = Some(key);
|
||||
}
|
||||
|
||||
pub fn expecting_object_key(&self) -> bool {
|
||||
matches!(self.top().kind, ContainerKind::Object) && self.top().current_key.is_none()
|
||||
}
|
||||
|
||||
pub fn end_array(&mut self) -> Result<()> {
|
||||
match self.stack.pop() {
|
||||
Some(level) if level.kind == ContainerKind::Array => {
|
||||
self.add_value(level.node);
|
||||
Ok(())
|
||||
}
|
||||
Some(level) => {
|
||||
self.stack.push(level);
|
||||
Err(Error::Message("Unexpected array terminator".to_string()))
|
||||
}
|
||||
None => Err(Error::Message("Unexpected array terminator".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_object(&mut self) -> Result<()> {
|
||||
match self.stack.pop() {
|
||||
Some(level) if level.kind == ContainerKind::Object => {
|
||||
self.add_value(level.node);
|
||||
Ok(())
|
||||
}
|
||||
Some(level) => {
|
||||
self.stack.push(level);
|
||||
Err(Error::Message("Unexpected object terminator".to_string()))
|
||||
}
|
||||
None => Err(Error::Message("Unexpected object terminator".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_close(&mut self) {
|
||||
while self.stack.len() > 1 {
|
||||
let complete = {
|
||||
let top = self.top();
|
||||
match (top.kind, top.expected_size) {
|
||||
(ContainerKind::Array, Some(expected)) => {
|
||||
self.builder.array_len(top.node) == expected
|
||||
}
|
||||
(ContainerKind::Object, Some(expected)) => {
|
||||
self.builder.object_len(top.node) == expected && top.current_key.is_none()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
if !complete {
|
||||
break;
|
||||
}
|
||||
let level = self.stack.pop().expect("non-root stack level");
|
||||
self.add_value(level.node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<Graph> {
|
||||
if self.stack.len() > 1 {
|
||||
return Err(Error::Message("Unfinished container".to_string()));
|
||||
}
|
||||
let root_wrapper = self.stack.pop().expect("root stack level").node;
|
||||
let root = match &self.builder.nodes[root_wrapper.0] {
|
||||
Node::Array(items) if !items.is_empty() => items[0],
|
||||
_ => self.builder.add(Node::Null),
|
||||
};
|
||||
Ok(self.builder.finish(root))
|
||||
}
|
||||
|
||||
pub fn register_id(&mut self, id: i64, node: NodeId) {
|
||||
if let Some(id_map) = &mut self.id_map {
|
||||
id_map.insert(id, node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_reference(&self, id: i64) -> Result<NodeId> {
|
||||
self.id_map
|
||||
.as_ref()
|
||||
.and_then(|map| map.get(&id).copied())
|
||||
.ok_or_else(|| Error::Message(format!("got invalid id '{id}'")))
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
use crate::error::{Error, Result};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn reverse_u64(n: u64) -> u64 {
|
||||
n.swap_bytes()
|
||||
}
|
||||
|
||||
pub fn encode_double<W: Write>(mut w: W, value: f64) -> Result<usize> {
|
||||
encode(&mut w, reverse_u64(value.to_bits()) as i64)
|
||||
}
|
||||
|
||||
/// Encodes a signed 64-bit integer using the same zigzag + LEB128 scheme as
|
||||
/// `net.woggioni.jwo.Leb128`.
|
||||
pub fn encode<W: Write>(mut w: W, input: i64) -> Result<usize> {
|
||||
let mut bytes_written = 0;
|
||||
let mut number = if input >= 0 {
|
||||
(input as u64) << 1
|
||||
} else {
|
||||
((-(input + 1)) as u64) << 1 | 1
|
||||
};
|
||||
while number & 127 != number {
|
||||
w.write_all(&[((number & 127) as u8) | 128])?;
|
||||
bytes_written += 1;
|
||||
number >>= 7;
|
||||
}
|
||||
w.write_all(&[number as u8])?;
|
||||
Ok(bytes_written + 1)
|
||||
}
|
||||
|
||||
pub fn encode_to_vec(input: i64) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
encode(&mut out, input).expect("Vec write cannot fail");
|
||||
out
|
||||
}
|
||||
|
||||
pub struct Decoder<R> {
|
||||
r: R,
|
||||
bytes_read: usize,
|
||||
}
|
||||
|
||||
impl<R: Read> Decoder<R> {
|
||||
pub fn new(r: R) -> Self {
|
||||
Self { r, bytes_read: 0 }
|
||||
}
|
||||
|
||||
pub fn bytes_read(&self) -> usize {
|
||||
self.bytes_read
|
||||
}
|
||||
|
||||
pub fn decode(&mut self) -> Result<i64> {
|
||||
let mut res = 0u64;
|
||||
for i in 0..10 {
|
||||
let mut buf = [0u8; 1];
|
||||
let read = self.r.read(&mut buf)?;
|
||||
self.bytes_read += read;
|
||||
if read == 0 {
|
||||
return Err(Error::Message("Unexpected end of file".to_string()));
|
||||
}
|
||||
let c = buf[0];
|
||||
res |= ((c & 127) as u64) << (i * 7);
|
||||
if c & 128 == 0 {
|
||||
return Ok(if res & 1 != 0 {
|
||||
-((res >> 1) as i64) - 1
|
||||
} else {
|
||||
(res >> 1) as i64
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(Error::Message("Invalid LEB128 value".to_string()))
|
||||
}
|
||||
|
||||
pub fn decode_double(&mut self) -> Result<f64> {
|
||||
Ok(f64::from_bits(reverse_u64(self.decode()? as u64)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_slice(input: &[u8]) -> Result<(i64, usize)> {
|
||||
let mut decoder = Decoder::new(input);
|
||||
let value = decoder.decode()?;
|
||||
Ok((value, decoder.bytes_read()))
|
||||
}
|
||||
|
||||
pub fn decode_double_slice(input: &[u8]) -> Result<(f64, usize)> {
|
||||
let mut decoder = Decoder::new(input);
|
||||
let value = decoder.decode_double()?;
|
||||
Ok((value, decoder.bytes_read()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn round_trip(v: i64) {
|
||||
let bytes = encode_to_vec(v);
|
||||
let (decoded, read) = decode_slice(&bytes).unwrap();
|
||||
assert_eq!(decoded, v);
|
||||
assert_eq!(read, bytes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zigzag_round_trip() {
|
||||
for v in [
|
||||
0,
|
||||
1,
|
||||
-1,
|
||||
63,
|
||||
64,
|
||||
-64,
|
||||
127,
|
||||
128,
|
||||
-128,
|
||||
i64::MAX,
|
||||
i64::MIN + 1,
|
||||
] {
|
||||
round_trip(v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_zigzag_values() {
|
||||
assert_eq!(encode_to_vec(0), vec![0]);
|
||||
assert_eq!(encode_to_vec(-1), vec![1]);
|
||||
assert_eq!(encode_to_vec(1), vec![2]);
|
||||
assert_eq!(encode_to_vec(6), vec![12]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_round_trip() {
|
||||
for v in [0.0, -0.0, 1.0, -2.5, 36.0, 1e22, 1e-7] {
|
||||
let mut bytes = Vec::new();
|
||||
encode_double(&mut bytes, v).unwrap();
|
||||
let (decoded, read) = decode_double_slice(&bytes).unwrap();
|
||||
assert_eq!(decoded.to_bits(), v.to_bits());
|
||||
assert_eq!(read, bytes.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//! WSON text and JBON binary serialization.
|
||||
//!
|
||||
//! The serde API (`to_string`, `from_str`, `to_vec`, `from_slice`) supports the
|
||||
//! acyclic serde data model. WSON reference/cycle support is available through
|
||||
//! the graph API in [`text`] and [`binary`].
|
||||
|
||||
pub mod binary;
|
||||
pub mod config;
|
||||
pub mod de;
|
||||
pub mod error;
|
||||
pub mod graph;
|
||||
pub mod leb128;
|
||||
pub mod ser;
|
||||
pub mod text;
|
||||
pub mod value;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::{Error, Result};
|
||||
pub use graph::{Graph, Node, NodeId};
|
||||
pub use value::{Type, Value};
|
||||
|
||||
pub use binary::{from_slice, from_slice_with_config, to_vec, to_vec_with_config};
|
||||
pub use text::{from_str, from_str_with_config, to_string, to_string_with_config};
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::value::Value;
|
||||
use serde::ser::{
|
||||
self, Impossible, Serialize, SerializeMap, SerializeSeq, SerializeStruct,
|
||||
SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct ValueSerializer;
|
||||
|
||||
fn integer(value: i64) -> Result<Value> {
|
||||
Ok(Value::Integer(value))
|
||||
}
|
||||
|
||||
fn unsigned(value: u64) -> Result<Value> {
|
||||
i64::try_from(value)
|
||||
.map(Value::Integer)
|
||||
.map_err(|_| Error::Message("u64 value does not fit in WSON integer".to_string()))
|
||||
}
|
||||
|
||||
impl ser::Serializer for ValueSerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
type SerializeSeq = ArraySerializer;
|
||||
type SerializeTuple = ArraySerializer;
|
||||
type SerializeTupleStruct = ArraySerializer;
|
||||
type SerializeTupleVariant = TupleVariantSerializer;
|
||||
type SerializeMap = MapSerializer;
|
||||
type SerializeStruct = MapSerializer;
|
||||
type SerializeStructVariant = StructVariantSerializer;
|
||||
|
||||
fn serialize_bool(self, v: bool) -> Result<Value> {
|
||||
Ok(Value::Bool(v))
|
||||
}
|
||||
|
||||
fn serialize_i8(self, v: i8) -> Result<Value> {
|
||||
integer(v as i64)
|
||||
}
|
||||
|
||||
fn serialize_i16(self, v: i16) -> Result<Value> {
|
||||
integer(v as i64)
|
||||
}
|
||||
|
||||
fn serialize_i32(self, v: i32) -> Result<Value> {
|
||||
integer(v as i64)
|
||||
}
|
||||
|
||||
fn serialize_i64(self, v: i64) -> Result<Value> {
|
||||
integer(v)
|
||||
}
|
||||
|
||||
fn serialize_u8(self, v: u8) -> Result<Value> {
|
||||
unsigned(v as u64)
|
||||
}
|
||||
|
||||
fn serialize_u16(self, v: u16) -> Result<Value> {
|
||||
unsigned(v as u64)
|
||||
}
|
||||
|
||||
fn serialize_u32(self, v: u32) -> Result<Value> {
|
||||
unsigned(v as u64)
|
||||
}
|
||||
|
||||
fn serialize_u64(self, v: u64) -> Result<Value> {
|
||||
unsigned(v)
|
||||
}
|
||||
|
||||
fn serialize_f32(self, v: f32) -> Result<Value> {
|
||||
Ok(Value::Float(v as f64))
|
||||
}
|
||||
|
||||
fn serialize_f64(self, v: f64) -> Result<Value> {
|
||||
Ok(Value::Float(v))
|
||||
}
|
||||
|
||||
fn serialize_char(self, v: char) -> Result<Value> {
|
||||
Ok(Value::String(v.to_string()))
|
||||
}
|
||||
|
||||
fn serialize_str(self, v: &str) -> Result<Value> {
|
||||
Ok(Value::String(v.to_string()))
|
||||
}
|
||||
|
||||
fn serialize_bytes(self, _v: &[u8]) -> Result<Value> {
|
||||
Err(Error::Unsupported("bytes"))
|
||||
}
|
||||
|
||||
fn serialize_none(self) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Value> {
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
variant: &'static str,
|
||||
) -> Result<Value> {
|
||||
Ok(Value::String(variant.to_string()))
|
||||
}
|
||||
|
||||
fn serialize_newtype_struct<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
value: &T,
|
||||
) -> Result<Value> {
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
variant: &'static str,
|
||||
value: &T,
|
||||
) -> Result<Value> {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert(variant.to_string(), value.serialize(ValueSerializer)?);
|
||||
Ok(Value::Object(object))
|
||||
}
|
||||
|
||||
fn serialize_seq(self, len: Option<usize>) -> Result<ArraySerializer> {
|
||||
Ok(ArraySerializer {
|
||||
values: Vec::with_capacity(len.unwrap_or(0)),
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_tuple(self, len: usize) -> Result<ArraySerializer> {
|
||||
self.serialize_seq(Some(len))
|
||||
}
|
||||
|
||||
fn serialize_tuple_struct(self, _name: &'static str, len: usize) -> Result<ArraySerializer> {
|
||||
self.serialize_seq(Some(len))
|
||||
}
|
||||
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
variant: &'static str,
|
||||
len: usize,
|
||||
) -> Result<TupleVariantSerializer> {
|
||||
Ok(TupleVariantSerializer {
|
||||
variant,
|
||||
values: Vec::with_capacity(len),
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_map(self, _len: Option<usize>) -> Result<MapSerializer> {
|
||||
Ok(MapSerializer {
|
||||
entries: BTreeMap::new(),
|
||||
next_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<MapSerializer> {
|
||||
self.serialize_map(None)
|
||||
}
|
||||
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<StructVariantSerializer> {
|
||||
Ok(StructVariantSerializer {
|
||||
variant,
|
||||
entries: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_human_readable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArraySerializer {
|
||||
values: Vec<Value>,
|
||||
}
|
||||
|
||||
impl SerializeSeq for ArraySerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
self.values.push(value.serialize(ValueSerializer)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
Ok(Value::Array(self.values))
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeTuple for ArraySerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
SerializeSeq::serialize_element(self, value)
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
SerializeSeq::end(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeTupleStruct for ArraySerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
SerializeSeq::serialize_element(self, value)
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
SerializeSeq::end(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TupleVariantSerializer {
|
||||
variant: &'static str,
|
||||
values: Vec<Value>,
|
||||
}
|
||||
|
||||
impl SerializeTupleVariant for TupleVariantSerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
self.values.push(value.serialize(ValueSerializer)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert(self.variant.to_string(), Value::Array(self.values));
|
||||
Ok(Value::Object(object))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MapSerializer {
|
||||
entries: BTreeMap<String, Value>,
|
||||
next_key: Option<String>,
|
||||
}
|
||||
|
||||
impl SerializeMap for MapSerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<()> {
|
||||
self.next_key = Some(key.serialize(MapKeySerializer)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
let key = self.next_key.take().ok_or_else(|| {
|
||||
Error::Message("serialize_value called before serialize_key".to_string())
|
||||
})?;
|
||||
self.entries.insert(key, value.serialize(ValueSerializer)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
Ok(Value::Object(self.entries))
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializeStruct for MapSerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized + Serialize>(
|
||||
&mut self,
|
||||
key: &'static str,
|
||||
value: &T,
|
||||
) -> Result<()> {
|
||||
self.entries
|
||||
.insert(key.to_string(), value.serialize(ValueSerializer)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
Ok(Value::Object(self.entries))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StructVariantSerializer {
|
||||
variant: &'static str,
|
||||
entries: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
impl SerializeStructVariant for StructVariantSerializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized + Serialize>(
|
||||
&mut self,
|
||||
key: &'static str,
|
||||
value: &T,
|
||||
) -> Result<()> {
|
||||
self.entries
|
||||
.insert(key.to_string(), value.serialize(ValueSerializer)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert(self.variant.to_string(), Value::Object(self.entries));
|
||||
Ok(Value::Object(object))
|
||||
}
|
||||
}
|
||||
|
||||
struct MapKeySerializer;
|
||||
|
||||
fn key_must_be_string<T>() -> Result<T> {
|
||||
Err(Error::Message("map key must be a string".to_string()))
|
||||
}
|
||||
|
||||
impl ser::Serializer for MapKeySerializer {
|
||||
type Ok = String;
|
||||
type Error = Error;
|
||||
type SerializeSeq = Impossible<String, Error>;
|
||||
type SerializeTuple = Impossible<String, Error>;
|
||||
type SerializeTupleStruct = Impossible<String, Error>;
|
||||
type SerializeTupleVariant = Impossible<String, Error>;
|
||||
type SerializeMap = Impossible<String, Error>;
|
||||
type SerializeStruct = Impossible<String, Error>;
|
||||
type SerializeStructVariant = Impossible<String, Error>;
|
||||
|
||||
fn serialize_bool(self, v: bool) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_i8(self, v: i8) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_i16(self, v: i16) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_i32(self, v: i32) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_i64(self, v: i64) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_u8(self, v: u8) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_u16(self, v: u16) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_u32(self, v: u32) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_u64(self, v: u64) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_f32(self, _v: f32) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_f64(self, _v: f64) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_char(self, v: char) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_str(self, v: &str) -> Result<String> {
|
||||
Ok(v.to_string())
|
||||
}
|
||||
|
||||
fn serialize_bytes(self, _v: &[u8]) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_none(self) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<String> {
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_unit_struct(self, _name: &'static str) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
variant: &'static str,
|
||||
) -> Result<String> {
|
||||
Ok(variant.to_string())
|
||||
}
|
||||
|
||||
fn serialize_newtype_struct<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
value: &T,
|
||||
) -> Result<String> {
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_value: &T,
|
||||
) -> Result<String> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_seq(self, _len: Option<usize>) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_tuple(self, _len: usize) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_map(self, _len: Option<usize>) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Impossible<String, Error>> {
|
||||
key_must_be_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
use super::float_fmt::format_java_double;
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{Graph, Node, NodeId};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
|
||||
fn escape_string(value: &str, out: &mut String) {
|
||||
out.push('"');
|
||||
for c in value.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
_ => {
|
||||
if (c as u32) < 128 {
|
||||
out.push(c);
|
||||
} else if (c as u32) <= 0xffff {
|
||||
out.push_str(&format!("\\u{:04X}", c as u32));
|
||||
} else {
|
||||
// Rust strings cannot contain the UTF-16 surrogate halves Java emits.
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
fn count_occurrences(graph: &Graph) -> HashMap<NodeId, usize> {
|
||||
fn visit(graph: &Graph, id: NodeId, counts: &mut HashMap<NodeId, usize>) {
|
||||
let count = counts.entry(id).or_insert(0);
|
||||
*count += 1;
|
||||
if *count != 1 {
|
||||
return;
|
||||
}
|
||||
match graph.node(id) {
|
||||
Node::Array(items) => {
|
||||
for child in items {
|
||||
visit(graph, *child, counts);
|
||||
}
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
for child in entries.values() {
|
||||
visit(graph, *child, counts);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut counts = HashMap::new();
|
||||
visit(graph, graph.root(), &mut counts);
|
||||
counts
|
||||
}
|
||||
|
||||
fn assign_ids(graph: &Graph, counts: &HashMap<NodeId, usize>) -> HashMap<NodeId, usize> {
|
||||
fn visit(
|
||||
graph: &Graph,
|
||||
id: NodeId,
|
||||
counts: &HashMap<NodeId, usize>,
|
||||
seen: &mut HashSet<NodeId>,
|
||||
ids: &mut HashMap<NodeId, usize>,
|
||||
) {
|
||||
if counts.get(&id).copied().unwrap_or(0) > 1 && !ids.contains_key(&id) {
|
||||
let next = ids.len();
|
||||
ids.insert(id, next);
|
||||
}
|
||||
if !seen.insert(id) {
|
||||
return;
|
||||
}
|
||||
match graph.node(id) {
|
||||
Node::Array(items) => {
|
||||
for child in items {
|
||||
visit(graph, *child, counts, seen, ids);
|
||||
}
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
for child in entries.values() {
|
||||
visit(graph, *child, counts, seen, ids);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut ids = HashMap::new();
|
||||
let mut seen = HashSet::new();
|
||||
visit(graph, graph.root(), counts, &mut seen, &mut ids);
|
||||
ids
|
||||
}
|
||||
|
||||
struct DumpState<'a> {
|
||||
graph: &'a Graph,
|
||||
cfg: &'a Config,
|
||||
ids: HashMap<NodeId, usize>,
|
||||
dumped: HashSet<NodeId>,
|
||||
visiting: HashSet<NodeId>,
|
||||
}
|
||||
|
||||
impl<'a> DumpState<'a> {
|
||||
fn new(graph: &'a Graph, cfg: &'a Config) -> Self {
|
||||
let ids = if cfg.serialize_references {
|
||||
let counts = count_occurrences(graph);
|
||||
assign_ids(graph, &counts)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
Self {
|
||||
graph,
|
||||
cfg,
|
||||
ids,
|
||||
dumped: HashSet::new(),
|
||||
visiting: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_node(&mut self, id: NodeId, out: &mut String) -> Result<()> {
|
||||
let node = self.graph.node(id);
|
||||
if self.cfg.serialize_references {
|
||||
if let Some(reference_id) = self.ids.get(&id).copied() {
|
||||
if !self.dumped.insert(id) {
|
||||
out.push('$');
|
||||
out.push_str(&reference_id.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
out.push('(');
|
||||
out.push_str(&reference_id.to_string());
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
|
||||
match node {
|
||||
Node::Null => out.push_str("null"),
|
||||
Node::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
|
||||
Node::Integer(value) => out.push_str(&value.to_string()),
|
||||
Node::Float(value) => out.push_str(&format_java_double(*value)),
|
||||
Node::String(value) => escape_string(value, out),
|
||||
Node::Array(items) => {
|
||||
if !self.cfg.serialize_references && !self.visiting.insert(id) {
|
||||
return Err(Error::Cycle);
|
||||
}
|
||||
out.push('[');
|
||||
for (index, child) in items.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
self.dump_node(*child, out)?;
|
||||
}
|
||||
out.push(']');
|
||||
self.visiting.remove(&id);
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
if !self.cfg.serialize_references && !self.visiting.insert(id) {
|
||||
return Err(Error::Cycle);
|
||||
}
|
||||
out.push('{');
|
||||
for (index, (key, child)) in entries.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
escape_string(key, out);
|
||||
out.push(':');
|
||||
self.dump_node(*child, out)?;
|
||||
}
|
||||
out.push('}');
|
||||
self.visiting.remove(&id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump_graph(graph: &Graph, cfg: &Config) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
DumpState::new(graph, cfg).dump_node(graph.root(), &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn dump_graph_to_writer<W: Write>(graph: &Graph, cfg: &Config, mut writer: W) -> Result<()> {
|
||||
let text = dump_graph(graph, cfg)?;
|
||||
writer.write_all(text.as_bytes())?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::text::parser::parse_str;
|
||||
|
||||
#[test]
|
||||
fn compact_round_trip() {
|
||||
let cfg = Config::default();
|
||||
let graph = parse_str("{\"b\": [1, 2.5, null], \"a\": true}", &cfg).unwrap();
|
||||
assert_eq!(
|
||||
dump_graph(&graph, &cfg).unwrap(),
|
||||
"{\"a\":true,\"b\":[1,2.5,null]}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/// Formats `f64` with Java `Double.toString` semantics as closely as Rust's
|
||||
/// shortest-roundtrip formatting allows.
|
||||
pub fn format_java_double(value: f64) -> String {
|
||||
if value.is_nan() {
|
||||
return "NaN".to_string();
|
||||
}
|
||||
if value == f64::INFINITY {
|
||||
return "Infinity".to_string();
|
||||
}
|
||||
if value == f64::NEG_INFINITY {
|
||||
return "-Infinity".to_string();
|
||||
}
|
||||
if value == 0.0 {
|
||||
return if value.is_sign_negative() {
|
||||
"-0.0".to_string()
|
||||
} else {
|
||||
"0.0".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
let scientific = format!("{value:e}");
|
||||
let (mantissa, exp) = scientific
|
||||
.split_once('e')
|
||||
.expect("lowerexp formatting always contains e");
|
||||
let exponent: i32 = exp.parse().expect("lowerexp exponent is an integer");
|
||||
let negative = mantissa.starts_with('-');
|
||||
let mantissa = mantissa.trim_start_matches('-');
|
||||
let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
|
||||
|
||||
// Java uses plain decimal notation for 1e-3 <= abs(value) < 1e7.
|
||||
if (-3..7).contains(&exponent) {
|
||||
let point_pos = exponent + 1;
|
||||
let mut out = String::new();
|
||||
if negative {
|
||||
out.push('-');
|
||||
}
|
||||
if point_pos <= 0 {
|
||||
out.push_str("0.");
|
||||
for _ in 0..(-point_pos) {
|
||||
out.push('0');
|
||||
}
|
||||
out.push_str(&digits);
|
||||
} else if point_pos as usize >= digits.len() {
|
||||
out.push_str(&digits);
|
||||
for _ in 0..(point_pos as usize - digits.len()) {
|
||||
out.push('0');
|
||||
}
|
||||
out.push_str(".0");
|
||||
} else {
|
||||
let split = point_pos as usize;
|
||||
out.push_str(&digits[..split]);
|
||||
out.push('.');
|
||||
out.push_str(&digits[split..]);
|
||||
}
|
||||
out
|
||||
} else {
|
||||
let mut out = String::new();
|
||||
if negative {
|
||||
out.push('-');
|
||||
}
|
||||
out.push(digits.as_bytes()[0] as char);
|
||||
out.push('.');
|
||||
if digits.len() == 1 {
|
||||
out.push('0');
|
||||
} else {
|
||||
out.push_str(&digits[1..]);
|
||||
}
|
||||
out.push('E');
|
||||
out.push_str(&exponent.to_string());
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::format_java_double;
|
||||
|
||||
#[test]
|
||||
fn java_double_formatting() {
|
||||
assert_eq!(format_java_double(1.0), "1.0");
|
||||
assert_eq!(format_java_double(36.0), "36.0");
|
||||
assert_eq!(format_java_double(0.001), "0.001");
|
||||
assert_eq!(format_java_double(0.0001), "1.0E-4");
|
||||
assert_eq!(format_java_double(9_999_999.0), "9999999.0");
|
||||
assert_eq!(format_java_double(10_000_000.0), "1.0E7");
|
||||
assert_eq!(format_java_double(1e22), "1.0E22");
|
||||
assert_eq!(format_java_double(1e-7), "1.0E-7");
|
||||
assert_eq!(format_java_double(-2.5), "-2.5");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
pub mod dumper;
|
||||
pub mod float_fmt;
|
||||
pub mod parser;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::de::ValueDeserializer;
|
||||
use crate::error::Result;
|
||||
use crate::graph::Graph;
|
||||
use crate::ser::ValueSerializer;
|
||||
use crate::value::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub fn parse_str(input: &str, cfg: &Config) -> Result<Graph> {
|
||||
parser::parse_str(input, cfg)
|
||||
}
|
||||
|
||||
pub fn parse_reader<R: Read>(reader: R, cfg: &Config) -> Result<Graph> {
|
||||
parser::parse_reader(reader, cfg)
|
||||
}
|
||||
|
||||
pub fn dump_graph(graph: &Graph, cfg: &Config) -> Result<String> {
|
||||
dumper::dump_graph(graph, cfg)
|
||||
}
|
||||
|
||||
pub fn dump_graph_to_writer<W: Write>(graph: &Graph, cfg: &Config, writer: W) -> Result<()> {
|
||||
dumper::dump_graph_to_writer(graph, cfg, writer)
|
||||
}
|
||||
|
||||
pub fn to_string<T: ?Sized + Serialize>(value: &T) -> Result<String> {
|
||||
to_string_with_config(value, &Config::default())
|
||||
}
|
||||
|
||||
pub fn to_string_with_config<T: ?Sized + Serialize>(value: &T, cfg: &Config) -> Result<String> {
|
||||
let value = value.serialize(ValueSerializer)?;
|
||||
let graph = Graph::from_value(&value);
|
||||
dumper::dump_graph(&graph, cfg)
|
||||
}
|
||||
|
||||
pub fn to_writer<T: ?Sized + Serialize, W: Write>(value: &T, writer: W) -> Result<()> {
|
||||
to_writer_with_config(value, &Config::default(), writer)
|
||||
}
|
||||
|
||||
pub fn to_writer_with_config<T: ?Sized + Serialize, W: Write>(
|
||||
value: &T,
|
||||
cfg: &Config,
|
||||
mut writer: W,
|
||||
) -> Result<()> {
|
||||
let text = to_string_with_config(value, cfg)?;
|
||||
writer.write_all(text.as_bytes())?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn from_str<'a, T: Deserialize<'a>>(input: &'a str) -> Result<T> {
|
||||
from_str_with_config(input, &Config::default())
|
||||
}
|
||||
|
||||
pub fn from_str_with_config<'a, T: Deserialize<'a>>(input: &'a str, cfg: &Config) -> Result<T> {
|
||||
let graph = parser::parse_str(input, cfg)?;
|
||||
let value = Value::from_graph(&graph)?;
|
||||
T::deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
|
||||
pub fn from_reader<'a, T: Deserialize<'a>, R: Read>(reader: R) -> Result<T> {
|
||||
from_reader_with_config(reader, &Config::default())
|
||||
}
|
||||
|
||||
pub fn from_reader_with_config<'a, T: Deserialize<'a>, R: Read>(
|
||||
reader: R,
|
||||
cfg: &Config,
|
||||
) -> Result<T> {
|
||||
let graph = parser::parse_reader(reader, cfg)?;
|
||||
let value = Value::from_graph(&graph)?;
|
||||
T::deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{ContainerKind, Graph, Node, ParseStack};
|
||||
use std::io::Read;
|
||||
|
||||
pub(crate) struct TextParser<'a> {
|
||||
chars: Vec<char>,
|
||||
index: usize,
|
||||
current: Option<char>,
|
||||
line: usize,
|
||||
column: usize,
|
||||
cfg: &'a Config,
|
||||
}
|
||||
|
||||
impl<'a> TextParser<'a> {
|
||||
pub fn new(input: &str, cfg: &'a Config) -> Self {
|
||||
let mut parser = Self {
|
||||
chars: input.chars().collect(),
|
||||
index: 0,
|
||||
current: None,
|
||||
line: 1,
|
||||
column: 1,
|
||||
cfg,
|
||||
};
|
||||
parser.advance();
|
||||
parser
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Option<char> {
|
||||
if self.index >= self.chars.len() {
|
||||
self.current = None;
|
||||
return None;
|
||||
}
|
||||
let c = self.chars[self.index];
|
||||
self.index += 1;
|
||||
self.current = Some(c);
|
||||
if c == '\n' {
|
||||
self.line += 1;
|
||||
self.column = 1;
|
||||
} else {
|
||||
self.column += 1;
|
||||
}
|
||||
Some(c)
|
||||
}
|
||||
|
||||
fn error<T>(&self, message: impl Into<String>) -> Result<T> {
|
||||
Err(Error::text(self.line, self.column, message))
|
||||
}
|
||||
|
||||
fn is_blank(c: char) -> bool {
|
||||
matches!(c, ' ' | '\t' | '\n' | '\r')
|
||||
}
|
||||
|
||||
fn is_decimal(c: char) -> bool {
|
||||
matches!(c, '0'..='9' | '+' | '-' | '.' | 'e')
|
||||
}
|
||||
|
||||
fn parse_hex(&mut self) -> u32 {
|
||||
let mut result = 0u32;
|
||||
while let Some(c) = self.current {
|
||||
let digit = match c {
|
||||
'0'..='9' => c as u32 - '0' as u32,
|
||||
'a'..='f' => 10 + c as u32 - 'a' as u32,
|
||||
'A'..='F' => 10 + c as u32 - 'A' as u32,
|
||||
_ => break,
|
||||
};
|
||||
result = (result << 4).wrapping_add(digit);
|
||||
self.advance();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_number_text(&mut self) -> String {
|
||||
let mut out = String::new();
|
||||
while let Some(c) = self.current {
|
||||
if !Self::is_decimal(c) {
|
||||
break;
|
||||
}
|
||||
out.push(c);
|
||||
self.advance();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_id(&mut self) -> Result<i64> {
|
||||
let mut out = String::new();
|
||||
let mut digits_started = false;
|
||||
let mut digits_ended = false;
|
||||
while let Some(c) = self.current {
|
||||
if c == '(' {
|
||||
// ignored
|
||||
} else if c.is_whitespace() {
|
||||
if digits_started {
|
||||
digits_ended = true;
|
||||
}
|
||||
} else if c == ')' {
|
||||
break;
|
||||
} else if Self::is_decimal(c) {
|
||||
if digits_ended {
|
||||
return self.error("error parsing id");
|
||||
}
|
||||
digits_started = true;
|
||||
out.push(c);
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
out.parse::<i64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))
|
||||
}
|
||||
|
||||
fn read_string(&mut self) -> Result<String> {
|
||||
// current is the opening quote
|
||||
self.advance();
|
||||
let mut out = String::new();
|
||||
while let Some(c) = self.current {
|
||||
match c {
|
||||
'"' => {
|
||||
self.advance();
|
||||
break;
|
||||
}
|
||||
'\\' => {
|
||||
self.advance();
|
||||
let Some(escaped) = self.current else {
|
||||
break;
|
||||
};
|
||||
match escaped {
|
||||
'"' => out.push('"'),
|
||||
'r' => out.push('\r'),
|
||||
'n' => out.push('\n'),
|
||||
't' => out.push('\t'),
|
||||
'\\' => out.push('\\'),
|
||||
'u' => {
|
||||
self.advance();
|
||||
let code_point = self.parse_hex();
|
||||
match char::from_u32(code_point) {
|
||||
Some(ch) => out.push(ch),
|
||||
None => return self.error("invalid unicode escape"),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
other => {
|
||||
return self.error(format!("Unrecognized escape sequence '\\{other}'"))
|
||||
}
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
_ => {
|
||||
out.push(c);
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn consume_expected(&mut self, expected: &str, error_message: &str) -> Result<()> {
|
||||
for expected_char in expected.chars() {
|
||||
match self.current {
|
||||
None => return self.error("Unexpected end of stream"),
|
||||
Some(c) if c == expected_char => {
|
||||
self.advance();
|
||||
}
|
||||
_ => return self.error(error_message),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Result<Graph> {
|
||||
let mut stack = ParseStack::new(self.cfg);
|
||||
let mut current_id: Option<i64> = None;
|
||||
|
||||
while let Some(c) = self.current {
|
||||
if Self::is_blank(c) {
|
||||
self.advance();
|
||||
} else if c == '(' && self.cfg.serialize_references {
|
||||
current_id = Some(self.parse_id()?);
|
||||
} else if c == '{' {
|
||||
let node = stack.begin_object(None)?;
|
||||
if let Some(id) = current_id.take() {
|
||||
stack.register_id(id, node);
|
||||
}
|
||||
self.advance();
|
||||
} else if c == '}' {
|
||||
stack.end_object().map_err(|err| match err {
|
||||
Error::Message(msg) => Error::text(self.line, self.column, msg),
|
||||
other => other,
|
||||
})?;
|
||||
self.advance();
|
||||
} else if c == '[' {
|
||||
let node = stack.begin_array(None)?;
|
||||
if let Some(id) = current_id.take() {
|
||||
stack.register_id(id, node);
|
||||
}
|
||||
self.advance();
|
||||
} else if c == ']' {
|
||||
stack.end_array().map_err(|err| match err {
|
||||
Error::Message(msg) => Error::text(self.line, self.column, msg),
|
||||
other => other,
|
||||
})?;
|
||||
self.advance();
|
||||
} else if Self::is_decimal(c) {
|
||||
let text = self.parse_number_text();
|
||||
let node = if text.find('.').is_some_and(|pos| pos > 0) {
|
||||
let value = text
|
||||
.parse::<f64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
stack.builder.add(Node::Float(value))
|
||||
} else {
|
||||
let value = text
|
||||
.parse::<i64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
stack.builder.add(Node::Integer(value))
|
||||
};
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == '"' {
|
||||
let text = self.read_string()?;
|
||||
if stack.expecting_object_key() {
|
||||
stack.object_key(text);
|
||||
} else {
|
||||
let node = stack.builder.add(Node::String(text));
|
||||
stack.add_value(node);
|
||||
}
|
||||
continue;
|
||||
} else if c == 't' {
|
||||
self.consume_expected("true", "Unrecognized boolean value")?;
|
||||
let node = stack.builder.add(Node::Bool(true));
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == 'f' {
|
||||
self.consume_expected("false", "Unrecognized boolean value")?;
|
||||
let node = stack.builder.add(Node::Bool(false));
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == 'n' {
|
||||
self.consume_expected("null", "Unrecognized null value")?;
|
||||
let node = stack.builder.add(Node::Null);
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == '$' && stack.id_map.is_some() {
|
||||
self.advance();
|
||||
let text = self.parse_number_text();
|
||||
let id = text
|
||||
.parse::<i64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
let node = stack
|
||||
.resolve_reference(id)
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else {
|
||||
// The Java parser is lenient: commas, colons and unknown characters are skipped.
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
if stack.depth() > 1 {
|
||||
let missing = match stack.top().kind {
|
||||
ContainerKind::Array => ']',
|
||||
ContainerKind::Object => '}',
|
||||
};
|
||||
return self.error(format!("Missing '{missing}' token"));
|
||||
}
|
||||
stack.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_str(input: &str, cfg: &Config) -> Result<Graph> {
|
||||
TextParser::new(input, cfg).parse()
|
||||
}
|
||||
|
||||
pub fn parse_reader<R: Read>(mut reader: R, cfg: &Config) -> Result<Graph> {
|
||||
let mut input = String::new();
|
||||
reader.read_to_string(&mut input)?;
|
||||
parse_str(&input, cfg)
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::Graph;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Object,
|
||||
Array,
|
||||
String,
|
||||
Double,
|
||||
Integer,
|
||||
Boolean,
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Value {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
Array(Vec<Value>),
|
||||
Object(BTreeMap<String, Value>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn ty(&self) -> Type {
|
||||
match self {
|
||||
Value::Null => Type::Null,
|
||||
Value::Bool(_) => Type::Boolean,
|
||||
Value::Integer(_) => Type::Integer,
|
||||
Value::Float(_) => Type::Double,
|
||||
Value::String(_) => Type::String,
|
||||
Value::Array(_) => Type::Array,
|
||||
Value::Object(_) => Type::Object,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
matches!(self, Value::Null)
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Result<bool> {
|
||||
match self {
|
||||
Value::Bool(v) => Ok(*v),
|
||||
_ => Err(Error::Type("Not a boolean".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_i64(&self) -> Result<i64> {
|
||||
match self {
|
||||
Value::Integer(v) => Ok(*v),
|
||||
_ => Err(Error::Type("Not an integer".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_f64(&self) -> Result<f64> {
|
||||
match self {
|
||||
Value::Float(v) => Ok(*v),
|
||||
_ => Err(Error::Type("Not a float".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Result<&str> {
|
||||
match self {
|
||||
Value::String(v) => Ok(v),
|
||||
_ => Err(Error::Type("Not a String".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_array(&self) -> Result<&[Value]> {
|
||||
match self {
|
||||
Value::Array(v) => Ok(v),
|
||||
_ => Err(Error::Type("Not an array".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_object(&self) -> Result<&BTreeMap<String, Value>> {
|
||||
match self {
|
||||
Value::Object(v) => Ok(v),
|
||||
_ => Err(Error::Type("Not an object".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Result<usize> {
|
||||
match self {
|
||||
Value::Array(v) => Ok(v.len()),
|
||||
Value::Object(v) => Ok(v.len()),
|
||||
_ => Err(Error::Type("Neither an array nor an object".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_graph(graph: &Graph) -> Result<Value> {
|
||||
enum State {
|
||||
Visiting,
|
||||
Done,
|
||||
}
|
||||
fn convert(
|
||||
graph: &Graph,
|
||||
id: crate::graph::NodeId,
|
||||
states: &mut HashMap<crate::graph::NodeId, State>,
|
||||
) -> Result<Value> {
|
||||
match states.get(&id) {
|
||||
Some(State::Visiting) => return Err(Error::Cycle),
|
||||
Some(State::Done) => {
|
||||
// Shared acyclic nodes are duplicated in the tree representation.
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
states.insert(id, State::Visiting);
|
||||
let value = match graph.node(id) {
|
||||
crate::graph::Node::Null => Value::Null,
|
||||
crate::graph::Node::Bool(v) => Value::Bool(*v),
|
||||
crate::graph::Node::Integer(v) => Value::Integer(*v),
|
||||
crate::graph::Node::Float(v) => Value::Float(*v),
|
||||
crate::graph::Node::String(v) => Value::String(v.clone()),
|
||||
crate::graph::Node::Array(items) => Value::Array(
|
||||
items
|
||||
.iter()
|
||||
.map(|child| convert(graph, *child, states))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
),
|
||||
crate::graph::Node::Object(entries) => Value::Object(
|
||||
entries
|
||||
.iter()
|
||||
.map(|(k, child)| Ok((k.clone(), convert(graph, *child, states)?)))
|
||||
.collect::<Result<BTreeMap<_, _>>>()?,
|
||||
),
|
||||
};
|
||||
states.insert(id, State::Done);
|
||||
Ok(value)
|
||||
}
|
||||
convert(graph, graph.root(), &mut HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(value: bool) -> Self {
|
||||
Value::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Self {
|
||||
Value::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value {
|
||||
fn from(value: f64) -> Self {
|
||||
Value::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(value: String) -> Self {
|
||||
Value::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(value: &str) -> Self {
|
||||
Value::String(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Value {
|
||||
fn serialize<S: serde::Serializer>(
|
||||
&self,
|
||||
serializer: S,
|
||||
) -> std::result::Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
Value::Null => serializer.serialize_unit(),
|
||||
Value::Bool(value) => serializer.serialize_bool(*value),
|
||||
Value::Integer(value) => serializer.serialize_i64(*value),
|
||||
Value::Float(value) => serializer.serialize_f64(*value),
|
||||
Value::String(value) => serializer.serialize_str(value),
|
||||
Value::Array(items) => {
|
||||
use serde::ser::SerializeSeq;
|
||||
let mut seq = serializer.serialize_seq(Some(items.len()))?;
|
||||
for item in items {
|
||||
seq.serialize_element(item)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
Value::Object(entries) => {
|
||||
use serde::ser::SerializeMap;
|
||||
let mut map = serializer.serialize_map(Some(entries.len()))?;
|
||||
for (key, value) in entries {
|
||||
map.serialize_entry(key, value)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Value {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<Self, D::Error> {
|
||||
struct ValueVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for ValueVisitor {
|
||||
type Value = Value;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("any WSON value")
|
||||
}
|
||||
|
||||
fn visit_bool<E: serde::de::Error>(self, value: bool) -> std::result::Result<Value, E> {
|
||||
Ok(Value::Bool(value))
|
||||
}
|
||||
|
||||
fn visit_i64<E: serde::de::Error>(self, value: i64) -> std::result::Result<Value, E> {
|
||||
Ok(Value::Integer(value))
|
||||
}
|
||||
|
||||
fn visit_u64<E: serde::de::Error>(self, value: u64) -> std::result::Result<Value, E> {
|
||||
i64::try_from(value)
|
||||
.map(Value::Integer)
|
||||
.map_err(|_| E::custom("u64 value does not fit in WSON integer"))
|
||||
}
|
||||
|
||||
fn visit_f64<E: serde::de::Error>(self, value: f64) -> std::result::Result<Value, E> {
|
||||
Ok(Value::Float(value))
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> std::result::Result<Value, E> {
|
||||
Ok(Value::String(value.to_string()))
|
||||
}
|
||||
|
||||
fn visit_string<E: serde::de::Error>(
|
||||
self,
|
||||
value: String,
|
||||
) -> std::result::Result<Value, E> {
|
||||
Ok(Value::String(value))
|
||||
}
|
||||
|
||||
fn visit_none<E: serde::de::Error>(self) -> std::result::Result<Value, E> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn visit_unit<E: serde::de::Error>(self) -> std::result::Result<Value, E> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn visit_seq<A: serde::de::SeqAccess<'de>>(
|
||||
self,
|
||||
mut seq: A,
|
||||
) -> std::result::Result<Value, A::Error> {
|
||||
let mut items = Vec::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(item) = seq.next_element()? {
|
||||
items.push(item);
|
||||
}
|
||||
Ok(Value::Array(items))
|
||||
}
|
||||
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(
|
||||
self,
|
||||
mut map: A,
|
||||
) -> std::result::Result<Value, A::Error> {
|
||||
let mut entries = BTreeMap::new();
|
||||
while let Some((key, value)) = map.next_entry()? {
|
||||
entries.insert(key, value);
|
||||
}
|
||||
Ok(Value::Object(entries))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ValueVisitor)
|
||||
}
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"widget": {
|
||||
"debug": "on",
|
||||
"window": {
|
||||
"parent" : null,
|
||||
"title": "Sample Konfabulator Widget",
|
||||
"name": "main_window",
|
||||
"width": 500,
|
||||
"height": 501
|
||||
},
|
||||
"image": {
|
||||
"src": "Images/Sun.png",
|
||||
"name": "sun1",
|
||||
"hOffset": 250,
|
||||
"vOffset": 250,
|
||||
"alignment": "center",
|
||||
"tags" : ["Ireland", "Amazon", "development"],
|
||||
"monochromatic" : false
|
||||
},
|
||||
"text": {
|
||||
"data": "Click Here",
|
||||
"size": 36,
|
||||
"style": "bold",
|
||||
"name": "text1",
|
||||
"hOffset": 250,
|
||||
"vOffset": 100,
|
||||
"alignment": "center",
|
||||
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+229
@@ -0,0 +1,229 @@
|
||||
²v_linksh
|
||||
about°_href?https://www.sitepoint.com/wp-json/wp/v2/types/postauthor°`embeddablehref@https://www.sitepoint.com/wp-json/wp/v2/users/72596collection°_href:https://www.sitepoint.com/wp-json/wp/v2/postscuries°ahref$https://api.w.org/{rel}namewptemplatedreplies°`embeddablehrefIhttps://www.sitepoint.com/wp-json/wp/v2/comments?post=168697self°_hrefAhttps://www.sitepoint.com/wp-json/wp/v2/posts/168697version-history°_hrefKhttps://www.sitepoint.com/wp-json/wp/v2/posts/168697/revisionswp:attachment°_hrefHhttps://www.sitepoint.com/wp-json/wp/v2/media?parent=168697 wp:featuredmedia°`embeddablehrefAhttps://www.sitepoint.com/wp-json/wp/v2/media/168703wp:term±aembeddablehrefKhttps://www.sitepoint.com/wp-json/wp/v2/categories?post=168697taxonomycategoryaembeddablehrefEhttps://www.sitepoint.com/wp-json/wp/v2/tags?post=168697taxonomypost_tagauthor¨îcategories±À[²[comment_statusopencontent`protectedrendered]€—<p class="wp-special"><em>This article was created in partnership with <a href="https://bawmedia.com/" rel="nofollow">BAWMedia</a>. Thank you for supporting the partners who make SitePoint possible.</em></p>
|
||||
<p>A well-designed website can serve as a powerful marketing tool. These days, creating one for a small business is not distressing or expensive at all. However, it was just a few short years ago.</p>
|
||||
<p>Today, you can take advantage of the features provided by the best WordPress themes. There are special themes for small business-oriented websites. It's not difficult to find a website-building theme that matches a specific business. This can be a startup, a service provider, or some other venture.</p>
|
||||
<p>You undoubtedly want nothing but the best business theme, right? Check out those described below. Each possesses functional designs loaded with amazing features. They will help you create a thoroughly engaging website to promote a business.</p>
|
||||
<h2>1. <a href="http://themes.muffingroup.com/be/splash/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow">Be Theme</a></h2>
|
||||
<p><a href="http://themes.muffingroup.com/be/splash/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215231.png" alt="" width="1000" height="481" class="aligncenter size-full wp-image-168702" /></a></p>
|
||||
<p>We’ll start with Be Theme, a responsive, multipurpose WordPress theme that takes every small business need into account with its more than 370 pre-built websites. There's a multiplicity of small business WordPress themes in this pre-built website collection ? each one embedded with the functionality you need to establish an effective online presence, and fully customizable to meet your business and marketing needs.</p>
|
||||
<p>The range of business niches covered is impressive, With more pre-built websites being added every month it's destined to become even more so. Web designers like Be Theme because it allows them to create a website for most small business types in as little as four hours.</p>
|
||||
<p>Clients appreciate the rapid turnaround they receive and the ease in which changes or additions they have in mind can be accommodated.</p>
|
||||
<p>Be Theme, one of the best WordPress themes for small business websites is a ThemeForest top 5 best seller whose core features include easy to work with page-building tools, a multiplicity of design features and options, and great support.</p>
|
||||
<h2>2. <a href="http://bit.ly/2wxuUpA" rel="nofollow">Astra</a></h2>
|
||||
<p><a href="http://bit.ly/2wxuUpA" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215352.jpg" alt="" width="1000" height="491" class="aligncenter size-full wp-image-168703" /></a></p>
|
||||
<p>Astra is fast, fully customizable, and one of the best WordPress themes for business websites as well as for blogs and personal portfolios. Built with SEO in mind, Astra is responsive and WooCommerce ready ? mandatory features in today's online business environment. Its capabilities are easily extendible with premium addons and Astra can be used with most of the popular page builders. This free WP-based theme is definitely worth considering.</p>
|
||||
<h2 id="">3. <a href="http://bit.ly/2oiQkTS" rel="nofollow">The100</a></h2>
|
||||
<p><a href="http://bit.ly/2oiQkTS" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215473.jpg" alt="" width="1000" height="491" class="aligncenter size-full wp-image-168704" /></a></p>
|
||||
<p>A theme selected for WordPress for small businesses can be free or it can be a premium theme requiring an expenditure on your behalf. There are several excellent free themes on the market, and one of them is The100. While it is advertised as having premium-like features, bear in mind that free themes like this one generally can't compete with premium themes. Nevertheless, The100 is an easy-to-use WP theme that features a multiplicity of layouts and plenty of customization options.</p>
|
||||
<h2 id="">4. <a href="https://undsgn.com/uncode/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow">Uncode ? Creative Multiuse WordPress Theme</a></h2>
|
||||
<p><a href="https://undsgn.com/uncode/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215604.jpg" alt="" width="1000" height="415" class="aligncenter size-full wp-image-168705" /></a></p>
|
||||
<p>Uncode has proven to be one of the best WordPress themes for business websites. It's a multipurpose theme featuring 30+ homepage concepts designed to get designers and their clients off to a fast start on any small business website. Features include an enhanced version of the popular Visual Composer page builder, and an Adaptive Images System that enables mobile users to see what you want and expect them to see.</p>
|
||||
<h2 id="">5. <a href="http://houzez.co/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow">Houzez ? Highly Customizable Real Estate WordPress Theme</a></h2>
|
||||
<p><a href="http://houzez.co/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215715.jpg" alt="" width="1000" height="415" class="aligncenter size-full wp-image-168706" /></a></p>
|
||||
<p>Some WordPress themes are created with a specific purpose in mind. Houzez is a specialty theme offering the features and functionality realtors and real estate agencies look for to promote their businesses and their marketability. Houzez' features include advanced property search filters, IDX systems, property management functionality, and solid customer support.</p>
|
||||
<h2 id="">6. <a href="http://preview.themeforest.net/item/thegem-creative-multipurpose-highperformance-wordpress-theme/full_screen_preview/16061685?sort_priority_group=meta-smallbusiness-startups&utm_source=baw&utm_medium=listing&utm_campaign=smallbusiness" rel="nofollow">TheGem ? Creative Multi-Purpose High-Performance WordPress Theme</a></h2>
|
||||
<p><a href="http://preview.themeforest.net/item/thegem-creative-multipurpose-highperformance-wordpress-theme/full_screen_preview/16061685?sort_priority_group=meta-smallbusiness-startups&utm_source=baw&utm_medium=listing&utm_campaign=smallbusiness" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215846.jpg" alt="" width="1000" height="415" class="aligncenter size-full wp-image-168707" /></a></p>
|
||||
<p>TheGem is without doubt one of the best WordPress business themes on the market. Its users like working with the trendy design concepts the authors have presented based on their analysis of current UX trends. Visual Composer is TheGems' page builder, and a judiciously selected set of plugins gives the web designer the flexibility to satisfy any small business's needs. The package includes a ready-to-go online fashion store.</p>
|
||||
<h2 id="">7. <a href="https://cesis.co/ts/rs.php?theme=cesis&utm_source=bawmedia&utm_medium=article&utm_campaign=bawmedia_cesis_sep2018&utm_content=post" rel="nofollow">Cesis ? Responsive Multi-Purpose WordPress Theme</a></h2>
|
||||
<p><a href="https://cesis.co/ts/rs.php?theme=cesis&utm_source=bawmedia&utm_medium=article&utm_campaign=bawmedia_cesis_sep2018&utm_content=post" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215977.png" alt="" width="1000" height="415" class="aligncenter size-full wp-image-168708" /></a></p>
|
||||
<p>When you're searching among the best WordPress themes for small business websites, Cesis is definitely worth a closer look. Its easy-to-use interface combined with a host of design elements and options allows you to build virtually anything you want. This is an important attribute when working with small businesses and startups, each having their unique business model and branding style.</p>
|
||||
<h2 id="">8. <a href="http://wpdemos.themezaa.com/pofo/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow">Pofo – Creative Portfolio and Blog WordPress Theme</a></h2>
|
||||
<p><a href="http://wpdemos.themezaa.com/pofo/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361216138.jpg" alt="" width="1000" height="415" class="aligncenter size-full wp-image-168709" /></a></p>
|
||||
<p>Web designers in need of small business WordPress themes include those whose clients represent creative teams and agencies as well as individual artists. Pofo is an ideal choice with its portfolio, eCommerce and blog features, bundled plugins, and more than 150 pre-built design elements. This premium theme's package also includes a nice assortment of home pages and more than 200 demo pages. Pofo is fully responsive, visually stunning, highly flexible, SEO and loading speed optimized.</p>
|
||||
<h2 id="conclusion">Conclusion</h2>
|
||||
<p>Did you like this selection of the best WordPress themes for small business websites? It provides you with a wide range of options and merits close and careful study.</p>
|
||||
<p>You really can't make a bad choice. With a little extra effort, you should be able to walk away with a perfect WordPress theme. It can be ideal for creating a certain small business website you have in mind. Likewise, it can help you create a range of websites for small businesses.</p>
|
||||
date 2018-09-06T09:30:53date_gmt 2018-09-06T16:30:53excerpt`protectedrendered]ÂN<p class="wp-special"><em>This article was created in partnership with <a href="https://bawmedia.com/" rel="nofollow">BAWMedia</a>. Thank you for supporting the partners who make SitePoint possible.</em></p>
|
||||
<p>A well-designed website can serve as a powerful marketing tool. These days, creating one for a small business is not distressing or expensive at all. However, it was just a few short years ago.</p>
|
||||
<p>Today, you can take advantage of the features provided by the best WordPress themes. There are special themes for small business-oriented websites. It's not difficult to find a website-building theme that matches a specific business. This can be a startup, a service provider, or some other venture.</p>
|
||||
<p>You undoubtedly want nothing but the best business theme, right? Check out those described below. Each possesses functional designs loaded with amazing features. They will help you create a thoroughly engaging website to promote a business.</p>
|
||||
<h2>1. <a href="http://themes.muffingroup.com/be/splash/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow">Be Theme</a></h2>
|
||||
<p><a href="http://themes.muffingroup.com/be/splash/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215231.png" alt="" width="1000" height="481" class="aligncenter size-full wp-image-168702" /></a></p>
|
||||
<p>We’ll start with Be Theme, a responsive, multipurpose WordPress theme that takes every small business need into account with its more than 370 pre-built websites. There's a multiplicity of small business WordPress themes in this pre-built website collection ? each one embedded with the functionality you need to establish an effective online presence, and fully customizable to meet your business and marketing needs.</p>
|
||||
<p>The range of business niches covered is impressive, With more pre-built websites being added every month it's destined to become even more so. Web designers like Be Theme because it allows them to create a website for most small business types in as little as four hours.</p>
|
||||
<p>Clients appreciate the rapid turnaround they receive and the ease in which changes or additions they have in mind can be accommodated.</p>
|
||||
<p>Be Theme, one of the best WordPress themes for small business websites is a ThemeForest top 5 best seller whose core features include easy to work with page-building tools, a multiplicity of design features and options, and great support.</p>
|
||||
<h2>2. <a href="http://bit.ly/2wxuUpA" rel="nofollow">Astra</a></h2>
|
||||
<p><a href="http://bit.ly/2wxuUpA" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215352.jpg" alt="" width="1000" height="491" class="aligncenter size-full wp-image-168703" /></a></p>
|
||||
<p>Astra is fast, fully customizable, and one of the best WordPress themes for business websites as well as for blogs and personal portfolios. Built with SEO in mind, Astra is responsive and WooCommerce ready ? mandatory features in today's online business environment. Its capabilities are easily extendible with premium addons and Astra can be used with most of the popular page builders. This free WP-based theme is definitely worth considering.</p>
|
||||
<h2 id="">3. <a href="http://bit.ly/2oiQkTS" rel="nofollow">The100</a></h2>
|
||||
<p><a href="http://bit.ly/2oiQkTS" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215473.jpg" alt="" width="1000" height="491" class="aligncenter size-full wp-image-168704" /></a></p>
|
||||
<p>A theme selected for WordPress for small businesses can be free or it can be a premium theme requiring an expenditure on your behalf. There are several excellent free themes on the market, and one of them is The100. While it is advertised as having premium-like features, bear in mind that free themes like this one generally can't compete with premium themes. Nevertheless, The100 is an easy-to-use WP theme that features a multiplicity of layouts and plenty of customization options.</p>
|
||||
<h2 id="">4. <a href="https://undsgn.com/uncode/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow">Uncode ? Creative Multiuse WordPress Theme</a></h2>
|
||||
<p><a href="https://undsgn.com/uncode/?utm_source=sitepoint.com&utm_medium=content&utm_campaign=wpstartups18" rel="nofollow"><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/15361215604.jpg" alt="" width="1000" height="415" class="aligncenter size-full wp-image-168705" /></a></p>
|
||||
<p>Uncode has proven to be one of the best WordPress themes for business websites. It's a multipurpose theme featuring 30+ homepage concepts designed to get designers and their clients off to a fast start on any small business website. Features include an enhanced version of the popular Visual Composer page builder, and an Adaptive Images System that enables mobile users to see what you want and expect them to see.</p>
|
||||
featured_mediaþËformatstandardguid_rendered0https://www.sitepoint.com/?p=168697idòËlink]¤https://www.sitepoint.com/the-8-best-wordpress-themes-for-small-business-websites/meta¯modified 2018-09-04T21:31:02modified_gmt 2018-09-05T04:31:02ping_statusclosedslugDthe-8-best-wordpress-themes-for-small-business-websitesstatuspublishstickytags² ¶¢•´btemplate
|
||||
title_renderedDThe 8 Best WordPress Themes for Small Business Websitestypepostv_linksh
|
||||
about°_href?https://www.sitepoint.com/wp-json/wp/v2/types/postauthor°`embeddablehref@https://www.sitepoint.com/wp-json/wp/v2/users/72676collection°_href:https://www.sitepoint.com/wp-json/wp/v2/postscuries°ahref$https://api.w.org/{rel}namewptemplatedreplies°`embeddablehrefIhttps://www.sitepoint.com/wp-json/wp/v2/comments?post=168397self°_hrefAhttps://www.sitepoint.com/wp-json/wp/v2/posts/168397version-history°_hrefKhttps://www.sitepoint.com/wp-json/wp/v2/posts/168397/revisionswp:attachment°_hrefHhttps://www.sitepoint.com/wp-json/wp/v2/media?parent=168397 wp:featuredmedia°`embeddablehrefAhttps://www.sitepoint.com/wp-json/wp/v2/media/168458wp:term±aembeddablehrefKhttps://www.sitepoint.com/wp-json/wp/v2/categories?post=168397taxonomycategoryaembeddablehrefEhttps://www.sitepoint.com/wp-json/wp/v2/tags?post=168397taxonomypost_tagauthorÈïcategories°Ìcomment_statusopencontent`protectedrendered]ÐÃ<p class="wp-special"><em>This article was originally published on <a href="https://synd.co/2NCzyKk?" rel="canonical">MongoDB</a>. Thank you for supporting the partners who make SitePoint possible.</em></p>
|
||||
<p>You can build your online, operational workloads atop MongoDB and still respond to events in real time by kicking off <a href="https://aws.amazon.com/kinesis/">Amazon Kinesis</a> stream processing actions, using MongoDB Stitch Triggers.</p>
|
||||
<p>Let?s look at an example scenario in which a stream of data is being generated as a result of actions users take on a website. We?ll durably store the data and simultaneously feed a Kinesis process to do streaming analytics on something like cart abandonment, product recommendations, or even credit card fraud detection.</p>
|
||||
<p>We?ll do this by setting up a Stitch Trigger. When relevant data updates are made in MongoDB, the trigger will use a Stitch Function to call out to AWS Kinesis, as you can see in this architecture diagram:</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536043981image5-d4wtr8tn5c.png" alt="" width="1525" height="844" class="aligncenter size-full wp-image-168426" /></p>
|
||||
<h4 id="whatyoullneedtofollowalong">What you?ll need to follow along</h4>
|
||||
<ol>
|
||||
<li><strong>An Atlas instance</strong> <br />
|
||||
If you don?t already have an application running on Atlas, you can follow our <a href="https://docs.atlas.mongodb.com/getting-started/">getting started with Atlas guide here</a>. In this example, we?ll be using a database called <em><strong>streamdata</strong></em>, with a collection called <em><strong>clickdata</strong></em> where we?re writing data from our web-based e-commerce application.</li>
|
||||
<li><strong>An AWS account and a Kinesis stream</strong> <br />
|
||||
In this example, we?ll use a Kinesis stream to send data downstream to additional applications such as Kinesis Analytics. This is the stream we want to feed our updates into.</li>
|
||||
<li><strong>A Stitch application</strong> <br />
|
||||
If you don?t already have a Stitch application, <a href="https://cloud.mongodb.com/user#/atlas/login">log into Atlas</a>, and click <strong>Stitch Apps</strong> from the navigation on the left, then click <strong>Create New Application</strong>.</li>
|
||||
</ol>
|
||||
<h3 id="createacollection">Create a Collection</h3>
|
||||
<p>The first step is to create a database and collection from the Stitch application console. Click <strong>Rules</strong> from the left navigation menu and click the <strong>Add Collection</strong> button. Type <strong>streamdata</strong> for the database and <strong>clickdata</strong> for the collection name. Select the template labeled Users can <strong>only read and write their own data</strong> and provide a field name where we?ll specify the user id.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044010image2-51q4gsi7u2.png" alt="Figure 2. Create a collection" width="1999" height="923" class="aligncenter size-full wp-image-168427" /></p>
|
||||
<h3 id="configuringstitchtotalktoaws">Configuring Stitch to Talk to AWS</h3>
|
||||
<p>Stitch lets you configure <em>Services</em> to interact with external <a href="https://docs.mongodb.com/stitch/reference/partner-services/amazon-service/">services such as AWS Kinesis</a>. Choose <strong>Services</strong> from the navigation on the left, and click the <strong>Add a Service</strong> button, select the AWS service and set <strong>AWS <a href="https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/">Access Key ID, and Secret Access Key</a></strong>.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044038image8-2a9rr3qc4k.png" alt="Figure 3. Service Configuration in Stitch" width="1999" height="1041" class="aligncenter size-full wp-image-168428" /></p>
|
||||
<p><em>Services</em> use <em>Rules</em> to specify what aspect of a service Stitch can use, and how. Add a rule which will enable that service to communicate with Kinesis by clicking the button labeled NEW RULE. Name the rule ?kinesis? as we?ll be using this specific rule to enable communication with AWS Kinesis. In the section marked Action, select the API labeled Kinesis and select All Actions.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044067image1-h45v2y48z7.gif" alt="Figure 4. Add a rule to enable integration with Kinesis" width="1424" height="746" class="aligncenter size-full wp-image-168429" /></p>
|
||||
<h3 id="writeafunctionthatstreamsdocumentsintokinesis">Write a Function that Streams Documents into Kinesis</h3>
|
||||
<p>Now that we have a working AWS service, we can use it to put records into a Kinesis stream. The way we do that in Stitch is with Functions. Let?s set up a <em>putKinesisRecord</em> function.</p>
|
||||
<p>Select Functions from the left-hand menu, and click Create New Function. Provide a name for the function and paste the following in the body of the function.</p>
|
||||
<p><img src="https://webassets.mongodb.com/_com_assets/cms/image6-7wtiny60ji.gif" alt="Figure 5. Example Function - putKinesisRecord" /></p>
|
||||
<pre><code class="javascript language-javascript">exports = function(event){
|
||||
const awsService = context.services.get('aws');
|
||||
try{
|
||||
awsService.kinesis().PutRecord({
|
||||
Data: JSON.stringify(event.fullDocument),
|
||||
StreamName: "stitchStream",
|
||||
PartitionKey: "1"
|
||||
}).then(function(response) {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
catch(error){
|
||||
console.log(JSON.parse(error));
|
||||
}
|
||||
};
|
||||
</code></pre>
|
||||
<h3 id="testoutthefunction">Test Out the Function</h3>
|
||||
<p>Let?s make sure everything is working by calling that function manually. From the <strong>Function Editor</strong>, Click <strong>Console</strong> to view the interactive javascript console for Stitch.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044162image9-utm60qwobl.png" alt="" width="1999" height="947" class="aligncenter size-full wp-image-168430" /></p>
|
||||
<p>Functions called from Triggers require an event. To test execution of our function, we?ll need to pass a dummy event to the function. Creating variables from the console in Stitch is simple. Simply set the value of the variable to a JSON document. For our simple example, use the following:</p>
|
||||
<pre><code class="javascript language-javascript">event = {
|
||||
"operationType": "replace",
|
||||
"fullDocument": {
|
||||
"color": "black",
|
||||
"inventory": {
|
||||
"$numberInt": "1"
|
||||
},
|
||||
"overview": "test document",
|
||||
"price": {
|
||||
"$numberDecimal": "123"
|
||||
},
|
||||
"type": "backpack"
|
||||
},
|
||||
"ns": {
|
||||
"db": "streamdata",
|
||||
"coll": "clickdata"
|
||||
}
|
||||
}
|
||||
exports(event);
|
||||
</code></pre>
|
||||
<p>Paste the above into the console and click the button labeled <strong>Run Function As</strong>. Select a user and the function will execute.</p>
|
||||
<p>Ta-da!</p>
|
||||
<h3 id="puttingittogetherwithstitchtriggers">Putting It Together with Stitch Triggers</h3>
|
||||
<p>We?ve got our MongoDB collection living in Atlas, receiving events from our web app. We?ve got our Kinesis stream ready for data. We?ve got a Stitch Function that can put data into a Kinesis stream.</p>
|
||||
<p><a href="https://docs.mongodb.com/stitch/mongodb/triggers/">Configuring Stitch Triggers</a> is so simple it?s almost anticlimactic. Click <strong>Triggers</strong> from the left navigation, name your trigger, provide the database and collection context, and select the database events Stitch will react to with execution of a function.</p>
|
||||
<p>For the database and collection, use the names from step one. Now we?ll set the operations we want to watch with our trigger. (Some triggers might care about all of them ? inserts, updates, deletes, and replacements ? while others can be more efficient because they logically can only matter for some of those.) In our case, we?re going to watch for insert, update and replace operations.</p>
|
||||
<p>Now we specify our <em>putKinesisRecord</em> function as the linked function, and we?re done.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044235image4-mkz3w061j6.gif" alt="Figure 6. Trigger Configuration in Stitch" width="1430" height="928" class="aligncenter size-full wp-image-168431" /></p>
|
||||
<p>As part of trigger execution, Stitch will forward details associated with the trigger event, including the full document involved in the event (i.e. the newly inserted, updated, or deleted document from the collection.) This is where we can evaluate some condition or attribute of the incoming document and decide whether or not to put the record onto a stream.</p>
|
||||
<h3 id="testthetrigger">Test the Trigger!</h3>
|
||||
<p>Amazon provides a dashboard which will enable you to view details associated with the data coming into your stream.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044263image3-fbdbnbh0zx.png" alt="Figure 7. Kinesis Stream Monitoring" width="1878" height="1021" class="aligncenter size-full wp-image-168432" /></p>
|
||||
<p>As you execute the function from within Stitch, you?ll begin to see the data entering the Kinesis stream.</p>
|
||||
<h3 id="buildingmorefunctionality">Building More Functionality</h3>
|
||||
<p>So far our trigger is pretty basic ? it watches a collection and when any updates or inserts happen, it feeds the entire document to our Kinesis stream. From here we can build out some more intelligent functionality. To wrap up this post, let?s look at what we can do with the data once it?s been durably stored in MongoDB and placed into a stream.</p>
|
||||
<p>Once the record is in the Kinesis Stream you can configure additional services downstream to act on the data. A common use case incorporates Amazon Kinesis Data Analytics to perform analytics on the streaming data. <a href="https://aws.amazon.com/kinesis/data-analytics/">Amazon Kinesis Data Analytics</a> offers pre-configured templates to accomplish things like anomaly detection, simple alerts, aggregations, and more.</p>
|
||||
<p>For example, our stream of data will contain orders resulting from purchases. These orders may originate from point-of-sale systems, as well as from our web-based e-commerce application. Kinesis Analytics can be leveraged to create applications that process the incoming stream of data. For our example, we could build a <a href="https://docs.aws.amazon.com/kinesisanalytics/latest/dev/app-anomaly-detection.html">machine learning algorithm</a> to detect anomalies in the data or create a product performance leaderboard from a <a href="https://docs.aws.amazon.com/kinesisanalytics/latest/dev/sliding-window-concepts.html">sliding</a>, or <a href="https://docs.aws.amazon.com/kinesisanalytics/latest/dev/tumbling-window-concepts.html">tumbling</a> window of data from our stream.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044289image7-5c0nd1hscb.png" alt="Figure 8. Amazon Data Analytics - Anomaly Detection Example" width="1203" height="676" class="aligncenter size-full wp-image-168433" /></p>
|
||||
<h3 id="wrappingup">Wrapping Up</h3>
|
||||
<p>Now you can connect MongoDB to Kinesis. From here, you?re able to leverage any one of the many services offered from Amazon Web Services to build on your application. In our next article in the series, we?ll focus on getting the data back from Kinesis into MongoDB. In the meantime, let us know what you?re building with Atlas, Stitch, and Kinesis!</p>
|
||||
<h4 id="resources">Resources</h4>
|
||||
<p><strong>MongoDB Atlas</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://www.mongodb.com/presentations/tutorial-series-getting-started-with-mongodb-atlas">Getting Started</a> – Tutorial Playlist</li>
|
||||
<li><a href="https://www.mongodb.com/cloud/atlas/lp/general">Signup for Free</a></li>
|
||||
<li><a href="https://www.mongodb.com/cloud/atlas/faq">FAQ</a></li>
|
||||
</ul>
|
||||
<p><strong>MongoDB Stitch</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://docs.mongodb.com/stitch/getting-started/">Getting Started Documentation</a></li>
|
||||
<li><a href="https://docs.mongodb.com/stitch/tutorials/">MongoDB Stitch Tutorials</a></li>
|
||||
<li><a href="https://www.mongodb.com/collateral/mongodb-stitch-serverless-platform">MongoDB Stitch White Paper</a></li>
|
||||
<li><a href="https://www.mongodb.com/webinar/mongodb-stitch">Webinar ? 8th August 2018</a></li>
|
||||
</ul>
|
||||
<p><strong>Amazon Kinesis</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://docs.aws.amazon.com/streams/latest/dev/getting-started.html">Getting Started</a></li>
|
||||
<li><a href="https://aws.amazon.com/kinesis/data-streams/">Kinesis Data Streams</a></li>
|
||||
<li><a href="https://aws.amazon.com/kinesis/data-analytics/">Kinesis Data Analytics</a></li>
|
||||
</ul>
|
||||
date 2018-09-06T09:30:31date_gmt 2018-09-06T16:30:31excerpt`protectedrendered]¸Y<p class="wp-special"><em>This article was originally published on <a href="https://synd.co/2NCzyKk?" rel="canonical">MongoDB</a>. Thank you for supporting the partners who make SitePoint possible.</em></p>
|
||||
<p>You can build your online, operational workloads atop MongoDB and still respond to events in real time by kicking off <a href="https://aws.amazon.com/kinesis/">Amazon Kinesis</a> stream processing actions, using MongoDB Stitch Triggers.</p>
|
||||
<p>Let?s look at an example scenario in which a stream of data is being generated as a result of actions users take on a website. We?ll durably store the data and simultaneously feed a Kinesis process to do streaming analytics on something like cart abandonment, product recommendations, or even credit card fraud detection.</p>
|
||||
<p>We?ll do this by setting up a Stitch Trigger. When relevant data updates are made in MongoDB, the trigger will use a Stitch Function to call out to AWS Kinesis, as you can see in this architecture diagram:</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536043981image5-d4wtr8tn5c.png" alt="" width="1525" height="844" class="aligncenter size-full wp-image-168426" /></p>
|
||||
<h4 id="whatyoullneedtofollowalong">What you?ll need to follow along</h4>
|
||||
<ol>
|
||||
<li><strong>An Atlas instance</strong> <br />
|
||||
If you don?t already have an application running on Atlas, you can follow our <a href="https://docs.atlas.mongodb.com/getting-started/">getting started with Atlas guide here</a>. In this example, we?ll be using a database called <em><strong>streamdata</strong></em>, with a collection called <em><strong>clickdata</strong></em> where we?re writing data from our web-based e-commerce application.</li>
|
||||
<li><strong>An AWS account and a Kinesis stream</strong> <br />
|
||||
In this example, we?ll use a Kinesis stream to send data downstream to additional applications such as Kinesis Analytics. This is the stream we want to feed our updates into.</li>
|
||||
<li><strong>A Stitch application</strong> <br />
|
||||
If you don?t already have a Stitch application, <a href="https://cloud.mongodb.com/user#/atlas/login">log into Atlas</a>, and click <strong>Stitch Apps</strong> from the navigation on the left, then click <strong>Create New Application</strong>.</li>
|
||||
</ol>
|
||||
<h3 id="createacollection">Create a Collection</h3>
|
||||
<p>The first step is to create a database and collection from the Stitch application console. Click <strong>Rules</strong> from the left navigation menu and click the <strong>Add Collection</strong> button. Type <strong>streamdata</strong> for the database and <strong>clickdata</strong> for the collection name. Select the template labeled Users can <strong>only read and write their own data</strong> and provide a field name where we?ll specify the user id.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044010image2-51q4gsi7u2.png" alt="Figure 2. Create a collection" width="1999" height="923" class="aligncenter size-full wp-image-168427" /></p>
|
||||
<h3 id="configuringstitchtotalktoaws">Configuring Stitch to Talk to AWS</h3>
|
||||
<p>Stitch lets you configure <em>Services</em> to interact with external <a href="https://docs.mongodb.com/stitch/reference/partner-services/amazon-service/">services such as AWS Kinesis</a>. Choose <strong>Services</strong> from the navigation on the left, and click the <strong>Add a Service</strong> button, select the AWS service and set <strong>AWS <a href="https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/">Access Key ID, and Secret Access Key</a></strong>.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044038image8-2a9rr3qc4k.png" alt="Figure 3. Service Configuration in Stitch" width="1999" height="1041" class="aligncenter size-full wp-image-168428" /></p>
|
||||
<p><em>Services</em> use <em>Rules</em> to specify what aspect of a service Stitch can use, and how. Add a rule which will enable that service to communicate with Kinesis by clicking the button labeled NEW RULE. Name the rule ?kinesis? as we?ll be using this specific rule to enable communication with AWS Kinesis. In the section marked Action, select the API labeled Kinesis and select All Actions.</p>
|
||||
<p><img src="https://www.sitepoint.com/wp-content/uploads/2018/09/1536044067image1-h45v2y48z7.gif" alt="Figure 4. Add a rule to enable integration with Kinesis" width="1424" height="746" class="aligncenter size-full wp-image-168429" /></p>
|
||||
<h3 id="writeafunctionthatstreamsdocumentsintokinesis">Write a Function that Streams Documents into Kinesis</h3>
|
||||
<p>Now that we have a working AWS service, we can use it to put records into a Kinesis stream. The way we do that in Stitch is with Functions. Let?s set up a <em>putKinesisRecord</em> function.</p>
|
||||
<p>Select Functions from the left-hand menu, and click Create New Function. Provide a name for the function and paste the following in the body of the function.</p>
|
||||
<p><img src="https://webassets.mongodb.com/_com_assets/cms/image6-7wtiny60ji.gif" alt="Figure 5. Example Function - putKinesisRecord" /></p>
|
||||
<pre><code class="javascript language-javascript">exports = function(event){
|
||||
const awsService = context.services.get('aws');
|
||||
try{
|
||||
awsService.kinesis().PutRecord({
|
||||
Data: JSON.stringify(event.fullDocument),
|
||||
StreamName: "stitchStream",
|
||||
PartitionKey: "1"
|
||||
}).then(function(response) {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
catch(error){
|
||||
console.log(JSON.parse(error));
|
||||
}
|
||||
};
|
||||
</code></pre>
|
||||
<h3 id="testoutthefunction">Test Out the Function</h3>
|
||||
<p>Let?s make sure everything is working by calling that function manually. From the <strong>Function Editor</strong>, Click <strong>Console</strong> to view the interactive javascript console for Stitch.</p>
|
||||
featured_media”Èformatstandardguid_rendered0https://www.sitepoint.com/?p=168397idšÇlink]Âhttps://www.sitepoint.com/integrating-mongodb-and-amazon-kinesis-for-intelligent-durable-streams/meta¯modified 2018-09-04T00:20:42modified_gmt 2018-09-04T07:20:42ping_statusclosedslugSintegrating-mongodb-and-amazon-kinesis-for-intelligent-durable-streamsstatuspublishstickytags²¢•þ´btemplate
|
||||
title_renderedTIntegrating MongoDB and Amazon Kinesis for Intelligent, Durable Streamstypepostv_linksh
|
||||
about°_href?https://www.sitepoint.com/wp-json/wp/v2/types/postauthor°`embeddablehref@https://www.sitepoint.com/wp-json/wp/v2/users/72596collection°_href:https://www.sitepoint.com/wp-json/wp/v2/postscuries°ahref$https://api.w.org/{rel}namewptemplatedreplies°`embeddablehrefIhttps://www.sitepoint.com/wp-json/wp/v2/comments?post=167869self°_hrefAhttps://www.sitepoint.com/wp-json/wp/v2/posts/167869version-history°_hrefKhttps://www.sitepoint.com/wp-json/wp/v2/posts/167869/revisionswp:attachment°_hrefHhttps://www.sitepoint.com/wp-json/wp/v2/media?parent=167869 wp:featuredmedia°`embeddablehrefAhttps://www.sitepoint.com/wp-json/wp/v2/media/167879wp:term±aembeddablehrefKhttps://www.sitepoint.com/wp-json/wp/v2/categories?post=167869taxonomycategoryaembeddablehrefEhttps://www.sitepoint.com/wp-json/wp/v2/tags?post=167869taxonomypost_tagauthor¨îcategories±˜Ìcomment_statusopencontent`protectedrendered]â<p><iframe width="900" height="506" src="https://www.youtube.com/embed/c9YzHtgsiKE" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe></p>
|
||||
<p class="wp-special"><em>This article was originally published on <a href="https://resource.alibabacloud.com/webinar/live.htm?spm=a2c5p.11425181.0.0.4d913c86X8D869&webinarId=26" rel="canonical">Alibaba Cloud</a>. Thank you for supporting the partners who make SitePoint possible.</em></p>
|
||||
<p class="wp-special"><strong>Think you got a better tip for making the best use of Alibaba Cloud services? Tell us about it and go in for your chance to win a Macbook Pro (plus other cool stuff). <a href="https://www.sitepoint.com/alibaba-competition">Find out more here</a>.</strong></p>
|
||||
<p>Gain an introduction to ApsaraDB for RDS, a cloud-based relational database product provided by Alibaba Cloud. In this webinar you will watch over the shoulder of a Solution Architect and Trainer, as he covers the basic concepts and features of ApsaraDB for RDS including:</p>
|
||||
<ul>
|
||||
<li>HA feature (Master/Slave Architecture, Backup/Recovery, Temporary Instance)</li>
|
||||
<li>Scalability features (Read-only Instance)</li>
|
||||
<li>Security and Monitoring features</li>
|
||||
</ul>
|
||||
<p>This webinar is ideally suited for database engineers and beginners to the Alibaba Cloud product suite.</p>
|
||||
date 2018-09-05T09:30:47date_gmt 2018-09-05T16:30:47excerpt`protectedrendered]ž<p><iframe width="560" height="315" src="https://www.youtube.com/embed/c9YzHtgsiKE" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe></p>
|
||||
<p class="wp-special"><em>This article was originally published on <a href="https://resource.alibabacloud.com/webinar/live.htm?spm=a2c5p.11425181.0.0.4d913c86X8D869&webinarId=26" rel="canonical">Alibaba Cloud</a>. Thank you for supporting the partners who make SitePoint possible.</em></p>
|
||||
<p>Gain an introduction to ApsaraDB for RDS, a cloud-based relational database product provided by Alibaba Cloud. In this webinar you will watch over the shoulder of a Solution Architect and Trainer, as he covers the basic concepts and features of ApsaraDB for RDS including:</p>
|
||||
<ul>
|
||||
<li>HA feature (Master/Slave Architecture, Backup/Recovery, Temporary Instance)</li>
|
||||
<li>Scalability features (Read-only Instance)</li>
|
||||
<li>Security and Monitoring features</li>
|
||||
</ul>
|
||||
<p>This webinar is ideally suited for database engineers and beginners to the Alibaba Cloud product suite.</p>
|
||||
featured_mediaŽ¿formatstandardguid_rendered0https://www.sitepoint.com/?p=167869idú¾link]¬https://www.sitepoint.com/how-to-set-up-a-secure-relational-database-on-alibaba-cloud/meta¯modified 2018-09-06T01:21:52modified_gmt 2018-09-06T08:21:52ping_statusclosedslugHhow-to-set-up-a-secure-relational-database-on-alibaba-cloudstatuspublishstickytags±¦·¢•template
|
||||
title_renderedHHow to Set up a Secure Relational Database on Alibaba Cloudtypepost
|
||||
Vendored
+318
File diff suppressed because one or more lines are too long
+158
@@ -0,0 +1,158 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wson::binary;
|
||||
use wson::graph::Node;
|
||||
use wson::text;
|
||||
use wson::{Config, Graph, Value};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct Widget {
|
||||
name: String,
|
||||
width: i64,
|
||||
height: i64,
|
||||
enabled: bool,
|
||||
score: f64,
|
||||
tags: Vec<String>,
|
||||
child: Option<Box<Widget>>,
|
||||
}
|
||||
|
||||
fn widget() -> Widget {
|
||||
Widget {
|
||||
name: "main_window".to_string(),
|
||||
width: 500,
|
||||
height: 501,
|
||||
enabled: true,
|
||||
score: 36.0,
|
||||
tags: vec!["Ireland".to_string(), "Amazon".to_string()],
|
||||
child: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_text_round_trip() {
|
||||
let value = widget();
|
||||
let text = wson::to_string(&value).unwrap();
|
||||
let decoded: Widget = wson::from_str(&text).unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_binary_round_trip() {
|
||||
let value = widget();
|
||||
let bytes = wson::to_vec(&value).unwrap();
|
||||
let decoded: Widget = wson::from_slice(&bytes).unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_fixture_round_trip() {
|
||||
let cfg = Config::default();
|
||||
let input = include_str!("fixtures/test.json");
|
||||
let graph = text::parse_str(input, &cfg).unwrap();
|
||||
let dumped = text::dump_graph(&graph, &cfg).unwrap();
|
||||
let reparsed = text::parse_str(&dumped, &cfg).unwrap();
|
||||
assert_eq!(
|
||||
Value::from_graph(&graph).unwrap(),
|
||||
Value::from_graph(&reparsed).unwrap()
|
||||
);
|
||||
assert_eq!(dumped, text::dump_graph(&reparsed, &cfg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wordpress_json_to_jbon_round_trip() {
|
||||
let cfg = Config::default();
|
||||
let json = include_str!("fixtures/wordpress.json");
|
||||
let graph = text::parse_str(json, &cfg).unwrap();
|
||||
let bytes = binary::dump_graph(&graph, &cfg).unwrap();
|
||||
let reparsed = binary::parse_slice(&bytes, &cfg).unwrap();
|
||||
assert_eq!(
|
||||
Value::from_graph(&graph).unwrap(),
|
||||
Value::from_graph(&reparsed).unwrap()
|
||||
);
|
||||
assert_eq!(binary::dump_graph(&reparsed, &cfg).unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wordpress_jbon_byte_identical_round_trip() {
|
||||
let cfg = Config::default();
|
||||
let expected = include_bytes!("fixtures/wordpress.jbon");
|
||||
let graph = binary::parse_slice(expected, &cfg).unwrap();
|
||||
let actual = binary::dump_graph(&graph, &cfg).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_references_form_a_cycle() {
|
||||
let cfg = Config {
|
||||
serialize_references: true,
|
||||
..Config::default()
|
||||
};
|
||||
let graph = text::parse_str("(0){\"child\":$0,\"id\":25}", &cfg).unwrap();
|
||||
let root = graph.root();
|
||||
let child = match graph.node(root) {
|
||||
Node::Object(entries) => *entries.get("child").unwrap(),
|
||||
other => panic!("expected object, got {other:?}"),
|
||||
};
|
||||
assert_eq!(root, child);
|
||||
assert_eq!(
|
||||
text::dump_graph(&graph, &cfg).unwrap(),
|
||||
"(0){\"child\":$0,\"id\":25}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_references_form_a_cycle() {
|
||||
let cfg = Config {
|
||||
serialize_references: true,
|
||||
..Config::default()
|
||||
};
|
||||
let graph = text::parse_str("(0){\"child\":$0,\"id\":25}", &cfg).unwrap();
|
||||
let bytes = binary::dump_graph(&graph, &cfg).unwrap();
|
||||
let reparsed = binary::parse_slice(&bytes, &cfg).unwrap();
|
||||
let root = reparsed.root();
|
||||
let child = match reparsed.node(root) {
|
||||
Node::Object(entries) => *entries.get("child").unwrap(),
|
||||
other => panic!("expected object, got {other:?}"),
|
||||
};
|
||||
assert_eq!(root, child);
|
||||
assert_eq!(binary::dump_graph(&reparsed, &cfg).unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_graph_requires_references() {
|
||||
let nodes = vec![Node::Object({
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
entries.insert("self".to_string(), wson::NodeId(0));
|
||||
entries
|
||||
})];
|
||||
let graph = Graph::new(wson::NodeId(0), nodes);
|
||||
assert!(matches!(
|
||||
text::dump_graph(&graph, &Config::default()),
|
||||
Err(wson::Error::Cycle)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_depth_is_enforced() {
|
||||
let cfg = Config {
|
||||
max_depth: 8,
|
||||
serialize_references: false,
|
||||
};
|
||||
let input = "[[[[[[[[1]]]]]]]]";
|
||||
assert!(matches!(
|
||||
text::parse_str(input, &cfg),
|
||||
Err(wson::Error::MaxDepthExceeded { max_depth: 8 })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_implements_serde() {
|
||||
let value = Value::Object({
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert("answer".to_string(), Value::Integer(42));
|
||||
map
|
||||
});
|
||||
let text = wson::to_string(&value).unwrap();
|
||||
assert_eq!(text, "{\"answer\":42}");
|
||||
let decoded: Value = wson::from_str(&text).unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
Reference in New Issue
Block a user