use crate::{
index::{OutOfBoundsError, ParseIndexError},
Pointer,
};
use core::fmt::{self, Debug};
pub trait Assign {
type Value;
type Error;
fn assign<V>(&mut self, ptr: &Pointer, value: V) -> Result<Option<Self::Value>, Self::Error>
where
V: Into<Self::Value>;
}
#[derive(Debug, PartialEq, Eq)]
pub enum AssignError {
FailedToParseIndex {
offset: usize,
source: ParseIndexError,
},
OutOfBounds {
offset: usize,
source: OutOfBoundsError,
},
}
impl fmt::Display for AssignError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FailedToParseIndex { offset, .. } => {
write!(
f,
"assignment failed due to an invalid index at offset {offset}"
)
}
Self::OutOfBounds { offset, .. } => {
write!(
f,
"assignment failed due to index at offset {offset} being out of bounds"
)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AssignError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::FailedToParseIndex { source, .. } => Some(source),
Self::OutOfBounds { source, .. } => Some(source),
}
}
}
enum Assigned<'v, V> {
Done(Option<V>),
Continue { next_dest: &'v mut V, same_value: V },
}
#[cfg(feature = "json")]
mod json {
use super::{Assign, AssignError, Assigned};
use crate::{Pointer, Token};
use alloc::{
string::{String, ToString},
vec::Vec,
};
use core::mem;
use serde_json::{map::Entry, Map, Value};
fn expand(mut remaining: &Pointer, mut value: Value) -> Value {
while let Some((ptr, tok)) = remaining.split_back() {
remaining = ptr;
match tok.encoded() {
"0" | "-" => {
value = Value::Array(vec![value]);
}
_ => {
let mut obj = Map::new();
obj.insert(tok.to_string(), value);
value = Value::Object(obj);
}
}
}
value
}
impl Assign for Value {
type Value = Value;
type Error = AssignError;
fn assign<V>(&mut self, ptr: &Pointer, value: V) -> Result<Option<Self::Value>, Self::Error>
where
V: Into<Self::Value>,
{
assign_value(ptr, self, value.into())
}
}
pub(crate) fn assign_value(
mut ptr: &Pointer,
mut dest: &mut Value,
mut value: Value,
) -> Result<Option<Value>, AssignError> {
let mut offset = 0;
while let Some((token, tail)) = ptr.split_front() {
let tok_len = token.encoded().len();
let assigned = match dest {
Value::Array(array) => assign_array(token, tail, array, value, offset)?,
Value::Object(obj) => assign_object(token, tail, obj, value),
_ => assign_scalar(ptr, dest, value),
};
match assigned {
Assigned::Done(assignment) => {
return Ok(assignment);
}
Assigned::Continue {
next_dest: next_value,
same_value: same_src,
} => {
value = same_src;
dest = next_value;
ptr = tail;
}
}
offset += 1 + tok_len;
}
let replaced = Some(core::mem::replace(dest, value));
Ok(replaced)
}
#[allow(clippy::needless_pass_by_value)]
fn assign_array<'v>(
token: Token<'_>,
remaining: &Pointer,
array: &'v mut Vec<Value>,
src: Value,
offset: usize,
) -> Result<Assigned<'v, Value>, AssignError> {
let idx = token
.to_index()
.map_err(|source| AssignError::FailedToParseIndex { offset, source })?
.for_len_incl(array.len())
.map_err(|source| AssignError::OutOfBounds { offset, source })?;
debug_assert!(idx <= array.len());
if idx < array.len() {
if remaining.is_root() {
Ok(Assigned::Done(Some(mem::replace(&mut array[idx], src))))
} else {
Ok(Assigned::Continue {
next_dest: &mut array[idx],
same_value: src,
})
}
} else {
let src = expand(remaining, src);
array.push(src);
Ok(Assigned::Done(None))
}
}
#[allow(clippy::needless_pass_by_value)]
fn assign_object<'v>(
token: Token<'_>,
remaining: &Pointer,
obj: &'v mut Map<String, Value>,
src: Value,
) -> Assigned<'v, Value> {
let entry = obj.entry(token.to_string());
match entry {
Entry::Occupied(entry) => {
let entry = entry.into_mut();
if remaining.is_root() {
Assigned::Done(Some(mem::replace(entry, src)))
} else {
Assigned::Continue {
same_value: src,
next_dest: entry,
}
}
}
Entry::Vacant(entry) => {
entry.insert(expand(remaining, src));
Assigned::Done(None)
}
}
}
fn assign_scalar<'v>(
remaining: &Pointer,
scalar: &'v mut Value,
value: Value,
) -> Assigned<'v, Value> {
let replaced = Some(mem::replace(scalar, expand(remaining, value)));
Assigned::Done(replaced)
}
}
#[cfg(feature = "toml")]
mod toml {
use super::{Assign, AssignError, Assigned};
use crate::{Pointer, Token};
use alloc::{string::String, vec, vec::Vec};
use core::mem;
use toml::{map::Entry, map::Map, Value};
fn expand(mut remaining: &Pointer, mut value: Value) -> Value {
while let Some((ptr, tok)) = remaining.split_back() {
remaining = ptr;
match tok.encoded() {
"0" | "-" => {
value = Value::Array(vec![value]);
}
_ => {
let mut obj = Map::new();
obj.insert(tok.to_string(), value);
value = Value::Table(obj);
}
}
}
value
}
impl Assign for Value {
type Value = Value;
type Error = AssignError;
fn assign<V>(&mut self, ptr: &Pointer, value: V) -> Result<Option<Self::Value>, Self::Error>
where
V: Into<Self::Value>,
{
assign_value(ptr, self, value.into())
}
}
pub(crate) fn assign_value(
mut ptr: &Pointer,
mut dest: &mut Value,
mut value: Value,
) -> Result<Option<Value>, AssignError> {
let mut offset = 0;
while let Some((token, tail)) = ptr.split_front() {
let tok_len = token.encoded().len();
let assigned = match dest {
Value::Array(array) => assign_array(token, tail, array, value, offset)?,
Value::Table(tbl) => assign_object(token, tail, tbl, value),
_ => assign_scalar(ptr, dest, value),
};
match assigned {
Assigned::Done(assignment) => {
return Ok(assignment);
}
Assigned::Continue {
next_dest: next_value,
same_value: same_src,
} => {
value = same_src;
dest = next_value;
ptr = tail;
}
}
offset += 1 + tok_len;
}
let replaced = Some(mem::replace(dest, value));
Ok(replaced)
}
#[allow(clippy::needless_pass_by_value)]
fn assign_array<'v>(
token: Token<'_>,
remaining: &Pointer,
array: &'v mut Vec<Value>,
src: Value,
offset: usize,
) -> Result<Assigned<'v, Value>, AssignError> {
let idx = token
.to_index()
.map_err(|source| AssignError::FailedToParseIndex { offset, source })?
.for_len_incl(array.len())
.map_err(|source| AssignError::OutOfBounds { offset, source })?;
debug_assert!(idx <= array.len());
if idx < array.len() {
if remaining.is_root() {
Ok(Assigned::Done(Some(mem::replace(&mut array[idx], src))))
} else {
Ok(Assigned::Continue {
next_dest: &mut array[idx],
same_value: src,
})
}
} else {
let src = expand(remaining, src);
array.push(src);
Ok(Assigned::Done(None))
}
}
#[allow(clippy::needless_pass_by_value)]
fn assign_object<'v>(
token: Token<'_>,
remaining: &Pointer,
obj: &'v mut Map<String, Value>,
src: Value,
) -> Assigned<'v, Value> {
match obj.entry(token.to_string()) {
Entry::Occupied(entry) => {
let entry = entry.into_mut();
if remaining.is_root() {
Assigned::Done(Some(mem::replace(entry, src)))
} else {
Assigned::Continue {
same_value: src,
next_dest: entry,
}
}
}
Entry::Vacant(entry) => {
entry.insert(expand(remaining, src));
Assigned::Done(None)
}
}
}
fn assign_scalar<'v>(
remaining: &Pointer,
scalar: &'v mut Value,
value: Value,
) -> Assigned<'v, Value> {
Assigned::Done(Some(mem::replace(scalar, expand(remaining, value))))
}
}
#[cfg(test)]
#[allow(clippy::too_many_lines)]
mod tests {
use super::{Assign, AssignError};
use crate::{
index::{OutOfBoundsError, ParseIndexError},
Pointer,
};
use alloc::str::FromStr;
use core::fmt::{Debug, Display};
#[derive(Debug)]
struct Test<V: Assign> {
data: V,
ptr: &'static str,
assign: V,
expected_data: V,
expected: Result<Option<V>, V::Error>,
}
impl<V> Test<V>
where
V: Assign + Clone + PartialEq + Display + Debug,
V::Value: Debug + PartialEq + From<V>,
V::Error: Debug + PartialEq,
Result<Option<V>, V::Error>: PartialEq<Result<Option<V::Value>, V::Error>>,
{
fn all(tests: impl IntoIterator<Item = Test<V>>) {
tests.into_iter().enumerate().for_each(|(i, t)| t.run(i));
}
fn run(self, i: usize) {
let Test {
ptr,
mut data,
assign,
expected_data,
expected,
..
} = self;
let ptr = Pointer::from_static(ptr);
let replaced = ptr.assign(&mut data, assign.clone());
assert_eq!(
&expected_data, &data,
"test #{i}:\n\ndata: \n{data:#?}\n\nexpected_data\n{expected_data:#?}"
);
assert_eq!(&expected, &replaced);
}
}
#[test]
#[cfg(feature = "json")]
fn assign_json() {
use alloc::vec;
use serde_json::json;
Test::all([
Test {
ptr: "/foo",
data: json!({}),
assign: json!("bar"),
expected_data: json!({"foo": "bar"}),
expected: Ok(None),
},
Test {
ptr: "",
data: json!({"foo": "bar"}),
assign: json!("baz"),
expected_data: json!("baz"),
expected: Ok(Some(json!({"foo": "bar"}))),
},
Test {
ptr: "/foo",
data: json!({"foo": "bar"}),
assign: json!("baz"),
expected_data: json!({"foo": "baz"}),
expected: Ok(Some(json!("bar"))),
},
Test {
ptr: "/foo/bar",
data: json!({"foo": "bar"}),
assign: json!("baz"),
expected_data: json!({"foo": {"bar": "baz"}}),
expected: Ok(Some(json!("bar"))),
},
Test {
ptr: "/foo/bar",
data: json!({}),
assign: json!("baz"),
expected_data: json!({"foo": {"bar": "baz"}}),
expected: Ok(None),
},
Test {
ptr: "/",
data: json!({}),
assign: json!("foo"),
expected_data: json!({"": "foo"}),
expected: Ok(None),
},
Test {
ptr: "/-",
data: json!({}),
assign: json!("foo"),
expected_data: json!({"-": "foo"}),
expected: Ok(None),
},
Test {
ptr: "/-",
data: json!(null),
assign: json!(34),
expected_data: json!([34]),
expected: Ok(Some(json!(null))),
},
Test {
ptr: "/foo/-",
data: json!({"foo": "bar"}),
assign: json!("baz"),
expected_data: json!({"foo": ["baz"]}),
expected: Ok(Some(json!("bar"))),
},
Test {
ptr: "/foo/-/bar",
assign: "baz".into(),
data: json!({}),
expected: Ok(None),
expected_data: json!({"foo":[{"bar": "baz"}]}),
},
Test {
ptr: "/foo/-/bar",
assign: "qux".into(),
data: json!({"foo":[{"bar":"baz" }]}),
expected: Ok(None),
expected_data: json!({"foo":[{"bar":"baz"},{"bar":"qux"}]}),
},
Test {
ptr: "/foo/-/bar",
data: json!({"foo":[{"bar":"baz"},{"bar":"qux"}]}),
assign: "quux".into(),
expected: Ok(None),
expected_data: json!({"foo":[{"bar":"baz"},{"bar":"qux"},{"bar":"quux"}]}),
},
Test {
ptr: "/foo/0/bar",
data: json!({"foo":[{"bar":"baz"},{"bar":"qux"},{"bar":"quux"}]}),
assign: "grault".into(),
expected: Ok(Some("baz".into())),
expected_data: json!({"foo":[{"bar":"grault"},{"bar":"qux"},{"bar":"quux"}]}),
},
Test {
ptr: "/0",
data: json!({}),
assign: json!("foo"),
expected_data: json!({"0": "foo"}),
expected: Ok(None),
},
Test {
ptr: "/1",
data: json!(null),
assign: json!("foo"),
expected_data: json!({"1": "foo"}),
expected: Ok(Some(json!(null))),
},
Test {
ptr: "/0",
data: json!([]),
expected_data: json!(["foo"]),
assign: json!("foo"),
expected: Ok(None),
},
Test {
ptr: "///bar",
data: json!({"":{"":{"bar": 42}}}),
assign: json!(34),
expected_data: json!({"":{"":{"bar":34}}}),
expected: Ok(Some(json!(42))),
},
Test {
ptr: "/1",
data: json!([]),
assign: json!("foo"),
expected: Err(AssignError::OutOfBounds {
offset: 0,
source: OutOfBoundsError {
index: 1,
length: 0,
},
}),
expected_data: json!([]),
},
Test {
ptr: "/0",
data: json!(["foo"]),
assign: json!("bar"),
expected: Ok(Some(json!("foo"))),
expected_data: json!(["bar"]),
},
Test {
ptr: "/a",
data: json!([]),
assign: json!("foo"),
expected: Err(AssignError::FailedToParseIndex {
offset: 0,
source: ParseIndexError::InvalidInteger(usize::from_str("foo").unwrap_err()),
}),
expected_data: json!([]),
},
Test {
ptr: "/002",
data: json!([]),
assign: json!("foo"),
expected: Err(AssignError::FailedToParseIndex {
offset: 0,
source: ParseIndexError::LeadingZeros,
}),
expected_data: json!([]),
},
]);
}
#[test]
#[cfg(feature = "toml")]
fn assign_toml() {
use alloc::vec;
use toml::{toml, Table, Value};
Test::all([
Test {
data: Value::Table(toml::Table::new()),
ptr: "/foo",
assign: "bar".into(),
expected_data: toml! { "foo" = "bar" }.into(),
expected: Ok(None),
},
Test {
data: toml! {foo = "bar"}.into(),
ptr: "",
assign: "baz".into(),
expected_data: "baz".into(),
expected: Ok(Some(toml! {foo = "bar"}.into())),
},
Test {
data: toml! { foo = "bar"}.into(),
ptr: "/foo",
assign: "baz".into(),
expected_data: toml! {foo = "baz"}.into(),
expected: Ok(Some("bar".into())),
},
Test {
data: toml! { foo = "bar"}.into(),
ptr: "/foo/bar",
assign: "baz".into(),
expected_data: toml! {foo = { bar = "baz"}}.into(),
expected: Ok(Some("bar".into())),
},
Test {
data: Table::new().into(),
ptr: "/",
assign: "foo".into(),
expected_data: toml! {"" = "foo"}.into(),
expected: Ok(None),
},
Test {
data: Table::new().into(),
ptr: "/-",
assign: "foo".into(),
expected_data: toml! {"-" = "foo"}.into(),
expected: Ok(None),
},
Test {
data: "data".into(),
ptr: "/-",
assign: 34.into(),
expected_data: Value::Array(vec![34.into()]),
expected: Ok(Some("data".into())),
},
Test {
data: toml! {foo = "bar"}.into(),
ptr: "/foo/-",
assign: "baz".into(),
expected_data: toml! {foo = ["baz"]}.into(),
expected: Ok(Some("bar".into())),
},
Test {
data: Table::new().into(),
ptr: "/0",
assign: "foo".into(),
expected_data: toml! {"0" = "foo"}.into(),
expected: Ok(None),
},
Test {
data: 21.into(),
ptr: "/1",
assign: "foo".into(),
expected_data: toml! {"1" = "foo"}.into(),
expected: Ok(Some(21.into())),
},
Test {
data: Value::Array(vec![]),
ptr: "/0",
expected_data: vec![Value::from("foo")].into(),
assign: "foo".into(),
expected: Ok(None),
},
Test {
ptr: "/foo/-/bar",
assign: "baz".into(),
data: Table::new().into(),
expected: Ok(None),
expected_data: toml! { "foo" = [{"bar" = "baz"}] }.into(),
},
Test {
ptr: "/foo/-/bar",
assign: "qux".into(),
data: toml! {"foo" = [{"bar" = "baz"}] }.into(),
expected: Ok(None),
expected_data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}]}.into(),
},
Test {
ptr: "/foo/-/bar",
data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}]}.into(),
assign: "quux".into(),
expected: Ok(None),
expected_data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}, {"bar" = "quux"}]}
.into(),
},
Test {
ptr: "/foo/0/bar",
data: toml! {"foo" = [{"bar" = "baz"}, {"bar" = "qux"}, {"bar" = "quux"}]}.into(),
assign: "grault".into(),
expected: Ok(Some("baz".into())),
expected_data:
toml! {"foo" = [{"bar" = "grault"}, {"bar" = "qux"}, {"bar" = "quux"}]}.into(),
},
Test {
data: Value::Array(vec![]),
ptr: "/-",
assign: "foo".into(),
expected: Ok(None),
expected_data: vec!["foo"].into(),
},
Test {
data: Value::Array(vec![]),
ptr: "/1",
assign: "foo".into(),
expected: Err(AssignError::OutOfBounds {
offset: 0,
source: OutOfBoundsError {
index: 1,
length: 0,
},
}),
expected_data: Value::Array(vec![]),
},
Test {
data: Value::Array(vec![]),
ptr: "/a",
assign: "foo".into(),
expected: Err(AssignError::FailedToParseIndex {
offset: 0,
source: ParseIndexError::InvalidInteger(usize::from_str("foo").unwrap_err()),
}),
expected_data: Value::Array(vec![]),
},
]);
}
}