From 91b8d5c976bd940cc59d8df250101ff9a544a8fe Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Thu, 2 Nov 2023 22:55:17 +0000 Subject: [PATCH] feat: ability to quote keys to escape limitations Resolves #25 --- README.md | 5 +++- assets/inputs/quoted_keys.corn | 7 +++++ assets/outputs/json/quoted_keys.json | 10 +++++++ assets/outputs/toml/quoted_keys.toml | 7 +++++ assets/outputs/yaml/quoted_keys.yml | 7 +++++ libcorn/src/de.rs | 5 ++-- libcorn/src/grammar.pest | 15 ++++++++-- libcorn/src/lib.rs | 5 +++- libcorn/src/parser.rs | 43 +++++++++++++++++++--------- 9 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 assets/inputs/quoted_keys.corn create mode 100644 assets/outputs/json/quoted_keys.json create mode 100644 assets/outputs/toml/quoted_keys.toml create mode 100644 assets/outputs/yaml/quoted_keys.yml diff --git a/README.md b/README.md index 54613a6..efc7ed2 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,13 @@ Thanks to [A-Cloud-Ninja](https://github.com/A-Cloud-Ninja) for adding Lua suppo All Corn files must contain a top-level object that contains keys/values. -Keys do not require quotes around them. The first character in the key cannot be whitespace, +The first character in the key cannot be whitespace, a number or any of the following characters: `. - " $ { [ =`. The remaining characters can be any unicode character except whitespace and the following: `. =`. +Keys do not require quotes around them, although you can optionally use `single quotes` to avoid the above limitations and use any character in any position. +Within quoted keys, you can use `\'` to escape a quote. + Values must be one of the following: - String diff --git a/assets/inputs/quoted_keys.corn b/assets/inputs/quoted_keys.corn new file mode 100644 index 0000000..da61425 --- /dev/null +++ b/assets/inputs/quoted_keys.corn @@ -0,0 +1,7 @@ +{ + 'foo.bar' = 42 + 'green.eggs'.and.ham = "hello world" + 'with spaces' = true + 'escaped\'quote' = false + 'escaped=equals' = -3 +} \ No newline at end of file diff --git a/assets/outputs/json/quoted_keys.json b/assets/outputs/json/quoted_keys.json new file mode 100644 index 0000000..b26f31a --- /dev/null +++ b/assets/outputs/json/quoted_keys.json @@ -0,0 +1,10 @@ +{ + "foo.bar": 42, + "green.eggs": { + "and": { + "ham": "hello world" + } + }, + "with spaces": true, + "escaped'quote": false +} diff --git a/assets/outputs/toml/quoted_keys.toml b/assets/outputs/toml/quoted_keys.toml new file mode 100644 index 0000000..e7bf402 --- /dev/null +++ b/assets/outputs/toml/quoted_keys.toml @@ -0,0 +1,7 @@ +"foo.bar" = 42 +"with spaces" = true +"escaped'quote" = false + +["green.eggs".and] +ham = "hello world" + diff --git a/assets/outputs/yaml/quoted_keys.yml b/assets/outputs/yaml/quoted_keys.yml new file mode 100644 index 0000000..e311f05 --- /dev/null +++ b/assets/outputs/yaml/quoted_keys.yml @@ -0,0 +1,7 @@ +foo.bar: 42 +green.eggs: + and: + ham: hello world +with spaces: true +escaped'quote: false + diff --git a/libcorn/src/de.rs b/libcorn/src/de.rs index 8f36606..e643cde 100644 --- a/libcorn/src/de.rs +++ b/libcorn/src/de.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::VecDeque; use serde::de::{self, DeserializeSeed, EnumAccess, IntoDeserializer, VariantAccess, Visitor}; @@ -371,7 +370,7 @@ impl<'de> Map<'de> { Value::Object(values) => Self { values: values .into_iter() - .flat_map(|(key, value)| vec![Value::String(Cow::Borrowed(key)), value]) + .flat_map(|(key, value)| vec![Value::String(key), value]) .collect(), }, _ => unreachable!(), @@ -475,7 +474,7 @@ impl<'de> EnumAccess<'de> for Enum<'de> { Value::Object(obj) => { let first_pair = obj.into_iter().next(); if let Some(first_pair) = first_pair { - let value = Value::String(Cow::Borrowed(first_pair.0)); + let value = Value::String(first_pair.0); let tag = seed.deserialize(&mut Deserializer::from_value(value))?; Ok((tag, Variant::new(Some(first_pair.1)))) } else { diff --git a/libcorn/src/grammar.pest b/libcorn/src/grammar.pest index af09b5d..2cbe2c4 100644 --- a/libcorn/src/grammar.pest +++ b/libcorn/src/grammar.pest @@ -32,9 +32,20 @@ path = ${ ~ ( "." ~ path_seg )* } -path_seg = ${ path_char + } +path_seg = _{ + quoted_path_seg | regular_path_seg +} + +quoted_path_seg = ${ "'" ~ quoted_path_val ~ "'" } +quoted_path_val = ${ quoted_path_char + } +quoted_path_char = _{ + !("'" | "\\") ~ ANY + | "\\" ~ "'" +} + +regular_path_seg = ${ path_char + } -path_char = { !( WHITESPACE | "=" | "." ) ~ ANY } +path_char = _{ !( WHITESPACE | "=" | "." ) ~ ANY } value = _{ object | array | input | string | float | integer | boolean | null } diff --git a/libcorn/src/lib.rs b/libcorn/src/lib.rs index 22713bd..7ba402f 100644 --- a/libcorn/src/lib.rs +++ b/libcorn/src/lib.rs @@ -28,11 +28,14 @@ mod wasm; /// The names include their `$` prefix. pub type Inputs<'a> = HashMap<&'a str, Value<'a>>; +/// A map of keys to their values. +pub type Object<'a> = IndexMap, Value<'a>>; + #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum Value<'a> { /// Key/value map. Values can be mixed types. - Object(IndexMap<&'a str, Value<'a>>), + Object(Object<'a>), /// Array of values, can be mixed types. Array(Vec>), /// UTF-8 string diff --git a/libcorn/src/parser.rs b/libcorn/src/parser.rs index af80470..a8a0758 100644 --- a/libcorn/src/parser.rs +++ b/libcorn/src/parser.rs @@ -8,7 +8,7 @@ use pest::iterators::Pair; use pest::Parser; use crate::error::{Error, Result}; -use crate::{Inputs, Value}; +use crate::{Inputs, Object, Value}; #[derive(pest_derive::Parser)] #[grammar = "grammar.pest"] @@ -189,7 +189,7 @@ impl<'a> CornParser<'a> { /// /// An `IndexMap` is used to ensure keys /// always output in the same order. - fn parse_object(&self, block: Pair<'a, Rule>) -> Result>> { + fn parse_object(&self, block: Pair<'a, Rule>) -> Result> { assert_eq!(block.as_rule(), Rule::object); let mut obj = IndexMap::new(); @@ -198,21 +198,20 @@ impl<'a> CornParser<'a> { match pair.as_rule() { Rule::pair => { let mut path_rules = pair.into_inner(); + let path = path_rules .next() - .expect("object pairs should contain a key") - .as_str(); + .expect("object pairs should contain a key"); + + let paths = Self::parse_path(path); + let value = self.parse_value( path_rules .next() .expect("object pairs should contain a value"), )?; - obj = Self::add_at_path( - obj, - path.split('.').collect::>().as_slice(), - value, - )?; + obj = Self::add_at_path(obj, &paths, value)?; } Rule::spread => { let input = pair @@ -235,6 +234,22 @@ impl<'a> CornParser<'a> { Ok(obj) } + fn parse_path(path: Pair) -> Vec> { + path.into_inner() + .map(|pair| match pair.as_rule() { + Rule::regular_path_seg => Cow::Borrowed(pair.as_str()), + Rule::quoted_path_seg => Cow::Owned( + pair.into_inner() + .next() + .expect("quoted paths should contain an inner value") + .as_str() + .replace('\\', ""), + ), + _ => unreachable!(), + }) + .collect::>() + } + /// Adds `Value` at the `path` in `obj`. /// /// `path` is an array where each entry represents another object key, @@ -242,16 +257,16 @@ impl<'a> CornParser<'a> { /// /// Objects are created up to the required depth recursively. fn add_at_path( - mut obj: IndexMap<&'a str, Value<'a>>, - path: &[&'a str], + mut obj: Object<'a>, + path: &[Cow<'a, str>], value: Value<'a>, - ) -> Result>> { + ) -> Result> { let (part, path_rest) = path .split_first() .expect("paths should contain at least 1 segment"); if path_rest.is_empty() { - obj.insert(part, value); + obj.insert(part.clone(), value); return Ok(obj); } @@ -262,7 +277,7 @@ impl<'a> CornParser<'a> { match child_obj { Value::Object(map) => { obj.insert( - part, + part.clone(), Value::Object(Self::add_at_path(map, path_rest, value)?), );