use indexmap to preserve array order, implement serialize for Value

This commit is contained in:
Robin Appelman 2025-08-17 19:38:16 +02:00
commit 3bea91855b
5 changed files with 160 additions and 38 deletions

30
Cargo.lock generated
View file

@ -274,6 +274,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.9"
@ -306,12 +312,28 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "indexmap"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.16" version = "0.4.16"
@ -525,10 +547,11 @@ dependencies = [
[[package]] [[package]]
name = "php-literal-parser" name = "php-literal-parser"
version = "0.6.3" version = "0.7.0"
dependencies = [ dependencies = [
"clap", "clap",
"criterion", "criterion",
"indexmap",
"logos", "logos",
"maplit", "maplit",
"memchr", "memchr",
@ -536,6 +559,7 @@ dependencies = [
"parse-display", "parse-display",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json",
"thiserror", "thiserror",
] ]
@ -690,9 +714,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.132" version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "php-literal-parser" name = "php-literal-parser"
description = "parser for php literals" description = "parser for php literals"
version = "0.6.3" version = "0.7.0"
authors = ["Robin Appelman <robin@icewind.nl>"] authors = ["Robin Appelman <robin@icewind.nl>"]
edition = "2018" edition = "2018"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -16,6 +16,7 @@ memchr = "2.7.4"
serde = "1.0.214" serde = "1.0.214"
miette = "7.2.0" miette = "7.2.0"
parse-display = "0.9.1" parse-display = "0.9.1"
indexmap = "2.10.0"
[dev-dependencies] [dev-dependencies]
maplit = "1.0.2" maplit = "1.0.2"
@ -24,6 +25,7 @@ miette = { version = "7.2.0", features = ["fancy"] }
criterion = "0.5.1" criterion = "0.5.1"
clap = "=4.3.24" clap = "=4.3.24"
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.142"
[[bench]] [[bench]]
name = "parse" name = "parse"

View file

@ -55,14 +55,16 @@ mod string;
use crate::string::is_array_key_numeric; use crate::string::is_array_key_numeric;
pub use error::ParseError; pub use error::ParseError;
use indexmap::IndexMap;
use serde::de::{self, MapAccess, SeqAccess, Visitor}; use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer}; use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub use serde_impl::from_str; pub use serde_impl::from_str;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
use std::fmt::{Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::ops::Index; use std::ops::Index;
@ -78,11 +80,11 @@ use std::ops::Index;
/// or the key is not found /// or the key is not found
/// ///
/// ```rust /// ```rust
/// # use maplit::hashmap; /// # use indexmap::indexmap;
/// # use php_literal_parser::Value; /// # use php_literal_parser::Value;
/// # /// #
/// # fn main() { /// # fn main() {
/// let value = Value::Array(hashmap!{ /// let value = Value::Array(indexmap!{
/// "key".into() => "value".into(), /// "key".into() => "value".into(),
/// 10.into() => false.into() /// 10.into() => false.into()
/// }); /// });
@ -97,7 +99,7 @@ pub enum Value {
Int(i64), Int(i64),
Float(f64), Float(f64),
String(String), String(String),
Array(HashMap<Key, Value>), Array(IndexMap<Key, Value>),
Null, Null,
} }
@ -164,8 +166,8 @@ impl Value {
} }
} }
/// Convert the value into a hashmap if it is one /// Convert the value into a map if it is one
pub fn into_hashmap(self) -> Option<HashMap<Key, Value>> { pub fn into_map(self) -> Option<IndexMap<Key, Value>> {
match self { match self {
Value::Array(map) => Some(map), Value::Array(map) => Some(map),
_ => None, _ => None,
@ -291,9 +293,15 @@ impl From<&str> for Value {
} }
} }
impl From<IndexMap<Key, Value>> for Value {
fn from(value: IndexMap<Key, Value>) -> Self {
Value::Array(value)
}
}
impl From<HashMap<Key, Value>> for Value { impl From<HashMap<Key, Value>> for Value {
fn from(value: HashMap<Key, Value>) -> Self { fn from(value: HashMap<Key, Value>) -> Self {
Value::Array(value) Value::Array(value.into_iter().collect())
} }
} }
@ -324,6 +332,8 @@ pub enum Key {
} }
impl Hash for Key { impl Hash for Key {
// a hash implementation which doesn't include the enum discriminant
// that hash hash("foo") == hash(Key::String("foo"))
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
match self { match self {
Key::Int(int) => int.hash(state), Key::Int(int) => int.hash(state),
@ -350,6 +360,18 @@ impl From<&str> for Key {
} }
} }
impl Serialize for Key {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Key::Int(i) => serializer.serialize_i64(*i),
Key::String(s) => serializer.serialize_str(s),
}
}
}
impl Key { impl Key {
/// Check if the key is an integer /// Check if the key is an integer
pub fn is_int(&self) -> bool { pub fn is_int(&self) -> bool {
@ -487,8 +509,8 @@ impl Display for Key {
#[test] #[test]
fn test_index() { fn test_index() {
use maplit::hashmap; use indexmap::indexmap;
let map = Value::Array(hashmap! { let map = Value::Array(indexmap! {
Key::String("key".to_string()) => Value::String("value".to_string()), Key::String("key".to_string()) => Value::String("value".to_string()),
Key::Int(1) => Value::Bool(true), Key::Int(1) => Value::Bool(true),
}); });
@ -624,7 +646,7 @@ impl<'de> Visitor<'de> for ValueVisitor {
where where
A: SeqAccess<'de>, A: SeqAccess<'de>,
{ {
let mut result = HashMap::new(); let mut result = IndexMap::new();
let mut next_key = 0; let mut next_key = 0;
while let Some(value) = seq.next_element::<Value>()? { while let Some(value) = seq.next_element::<Value>()? {
let key = Key::Int(next_key); let key = Key::Int(next_key);
@ -638,7 +660,7 @@ impl<'de> Visitor<'de> for ValueVisitor {
where where
A: MapAccess<'de>, A: MapAccess<'de>,
{ {
let mut result = HashMap::new(); let mut result = IndexMap::new();
while let Some((key, value)) = map.next_entry()? { while let Some((key, value)) = map.next_entry()? {
result.insert(key, value); result.insert(key, value);
} }
@ -655,6 +677,40 @@ impl<'de> Deserialize<'de> for Value {
} }
} }
impl Serialize for Value {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Value::Bool(b) => serializer.serialize_bool(*b),
Value::Int(i) => serializer.serialize_i64(*i),
Value::Float(f) => serializer.serialize_f64(*f),
Value::String(s) => serializer.serialize_str(s),
Value::Array(a) => {
if a.keys()
.enumerate()
.all(|(i, k)| k.as_int() == Some(i as i64))
{
let mut seq = serializer.serialize_seq(Some(a.len()))?;
for value in a.values() {
seq.serialize_element(value)?;
}
seq.end()
} else {
let mut map = serializer.serialize_map(Some(a.len()))?;
for (key, value) in a {
map.serialize_key(key)?;
map.serialize_value(value)?;
}
map.end()
}
}
Value::Null => serializer.serialize_none(),
}
}
}
struct KeyVisitor; struct KeyVisitor;
impl<'de> Visitor<'de> for KeyVisitor { impl<'de> Visitor<'de> for KeyVisitor {

View file

@ -1,5 +1,6 @@
use indexmap::indexmap;
use miette::Report; use miette::Report;
use php_literal_parser::from_str; use php_literal_parser::{from_str, Key, Value};
use serde_derive::Deserialize; use serde_derive::Deserialize;
#[test] #[test]
@ -92,3 +93,42 @@ pub struct Route {
name: String, name: String,
verb: String, verb: String,
} }
#[test]
fn test_value_serde_roundtrip() {
fn test_roundtrip(val: Value) {
let json = serde_json::to_string(&val).unwrap();
let parsed: Value = serde_json::from_str(&json).unwrap();
assert_eq!(val, parsed);
}
test_roundtrip(Value::Bool(false));
test_roundtrip(Value::Bool(true));
test_roundtrip(Value::Int(12));
test_roundtrip(Value::Float(12.12));
test_roundtrip(Value::String("foo".into()));
test_roundtrip(Value::Null);
test_roundtrip(Value::Array(indexmap! {
Key::Int(0) => Value::Bool(true),
Key::Int(1) => Value::Int(12),
Key::Int(2) => Value::Int(-12),
}));
test_roundtrip(Value::Array(indexmap! {
Key::Int(1) => Value::Bool(true),
Key::Int(2) => Value::Int(12),
Key::Int(5) => Value::Int(-12),
}));
test_roundtrip(Value::Array(indexmap! {
Key::String("foo".into()) => Value::Bool(true),
Key::String("var".into()) => Value::Array(indexmap! {
Key::String("bar".into()) => Value::Bool(true),
Key::String("asd".into()) => Value::Int(12),
Key::Int(2) => Value::Array(indexmap! {
Key::Int(0) => Value::Bool(true),
Key::Int(1) => Value::Int(12),
Key::Int(2) => Value::Int(-12),
}),
}),
}));
}

View file

@ -1,4 +1,4 @@
use maplit::hashmap; use indexmap::indexmap;
use php_literal_parser::{from_str, Key, ParseError, Value}; use php_literal_parser::{from_str, Key, ParseError, Value};
fn parse(source: &str) -> Result<Value, ParseError> { fn parse(source: &str) -> Result<Value, ParseError> {
@ -22,9 +22,9 @@ fn test_parse_value() {
Value::String("test".to_string()), Value::String("test".to_string()),
parse(r#""test""#).unwrap() parse(r#""test""#).unwrap()
); );
assert_eq!(Value::Array(hashmap! {}), parse(r#"array()"#).unwrap()); assert_eq!(Value::Array(indexmap! {}), parse(r#"array()"#).unwrap());
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(0) => Value::Int(3), Key::Int(0) => Value::Int(3),
Key::Int(1) => Value::Int(4), Key::Int(1) => Value::Int(4),
Key::Int(2) => Value::Int(5), Key::Int(2) => Value::Int(5),
@ -32,7 +32,7 @@ fn test_parse_value() {
parse(r#"array(3,4,5)"#).unwrap() parse(r#"array(3,4,5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(0) => Value::Int(3), Key::Int(0) => Value::Int(3),
Key::Int(1) => Value::Int(4), Key::Int(1) => Value::Int(4),
Key::Int(2) => Value::Int(5), Key::Int(2) => Value::Int(5),
@ -40,7 +40,7 @@ fn test_parse_value() {
parse(r#"array(3,4,5,)"#).unwrap() parse(r#"array(3,4,5,)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(1) => Value::Int(3), Key::Int(1) => Value::Int(3),
Key::Int(3) => Value::Int(4), Key::Int(3) => Value::Int(4),
Key::Int(5) => Value::Int(5), Key::Int(5) => Value::Int(5),
@ -48,7 +48,7 @@ fn test_parse_value() {
parse(r#"array(1=>3,3=>4,5=>5)"#).unwrap() parse(r#"array(1=>3,3=>4,5=>5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(1) => Value::Int(3), Key::Int(1) => Value::Int(3),
Key::Int(2) => Value::Int(4), Key::Int(2) => Value::Int(4),
Key::Int(3) => Value::Int(5), Key::Int(3) => Value::Int(5),
@ -56,7 +56,7 @@ fn test_parse_value() {
parse(r#"array(1=>3,4,5)"#).unwrap() parse(r#"array(1=>3,4,5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(1) => Value::Int(3), Key::Int(1) => Value::Int(3),
Key::Int(2) => Value::Int(4), Key::Int(2) => Value::Int(4),
Key::Int(3) => Value::Int(5), Key::Int(3) => Value::Int(5),
@ -64,7 +64,7 @@ fn test_parse_value() {
parse(r#"array("1"=>3,4,5)"#).unwrap() parse(r#"array("1"=>3,4,5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(1) => Value::Int(3), Key::Int(1) => Value::Int(3),
Key::Int(2) => Value::Int(4), Key::Int(2) => Value::Int(4),
Key::Int(3) => Value::Int(5), Key::Int(3) => Value::Int(5),
@ -72,7 +72,7 @@ fn test_parse_value() {
parse(r#"array(1.5=>3,4,5)"#).unwrap() parse(r#"array(1.5=>3,4,5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(1) => Value::Int(3), Key::Int(1) => Value::Int(3),
Key::Int(2) => Value::Int(4), Key::Int(2) => Value::Int(4),
Key::Int(3) => Value::Int(5), Key::Int(3) => Value::Int(5),
@ -80,7 +80,7 @@ fn test_parse_value() {
parse(r#"array(true=>3,4,5)"#).unwrap() parse(r#"array(true=>3,4,5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(1) => Value::Int(3), Key::Int(1) => Value::Int(3),
Key::String("foo".into()) => Value::Int(4), Key::String("foo".into()) => Value::Int(4),
Key::Int(2) => Value::Int(5), Key::Int(2) => Value::Int(5),
@ -88,18 +88,18 @@ fn test_parse_value() {
parse(r#"array(1=>3,"foo" => 4,5)"#).unwrap() parse(r#"array(1=>3,"foo" => 4,5)"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::String("foo".into()) => Value::Bool(true), Key::String("foo".into()) => Value::Bool(true),
Key::String("nested".into()) => Value::Array(hashmap! { Key::String("nested".into()) => Value::Array(indexmap! {
Key::String("foo".into()) => Value::Bool(false), Key::String("foo".into()) => Value::Bool(false),
}), }),
}), }),
parse(r#"array("foo" => true, "nested" => array ('foo' => false))"#).unwrap() parse(r#"array("foo" => true, "nested" => array ('foo' => false))"#).unwrap()
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::String("foo".into()) => Value::Bool(true), Key::String("foo".into()) => Value::Bool(true),
Key::String("nested".into()) => Value::Array(hashmap! { Key::String("nested".into()) => Value::Array(indexmap! {
Key::String("foo".into()) => Value::Null, Key::String("foo".into()) => Value::Null,
}), }),
}), }),
@ -120,7 +120,7 @@ fn test_parse_value() {
assert_eq!(Value::Float(1234.5), parse(r#"12_34.5"#).unwrap()); assert_eq!(Value::Float(1234.5), parse(r#"12_34.5"#).unwrap());
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(2) => Value::Int(3), Key::Int(2) => Value::Int(3),
Key::String("foo".into()) => Value::Int(4), Key::String("foo".into()) => Value::Int(4),
Key::String("".into()) => Value::Int(5), Key::String("".into()) => Value::Int(5),
@ -131,11 +131,11 @@ fn test_parse_value() {
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(0) => hashmap! { Key::Int(0) => indexmap! {
Key::String("a".into()) => Value::Int(2), Key::String("a".into()) => Value::Int(2),
}.into(), }.into(),
Key::Int(1) => hashmap! { Key::Int(1) => indexmap! {
Key::String("b".into()) => Value::Int(3), Key::String("b".into()) => Value::Int(3),
}.into() }.into()
}), }),
@ -143,11 +143,11 @@ fn test_parse_value() {
); );
assert_eq!( assert_eq!(
Value::Array(hashmap! { Value::Array(indexmap! {
Key::Int(0) => hashmap! { Key::Int(0) => indexmap! {
Key::String("a".into()) => Value::Int(2), Key::String("a".into()) => Value::Int(2),
}.into(), }.into(),
Key::Int(1) => hashmap! { Key::Int(1) => indexmap! {
Key::String("b".into()) => Value::Int(3), Key::String("b".into()) => Value::Int(3),
}.into() }.into()
}), }),