Skip to content

Commit

Permalink
feat: ability to quote keys to escape limitations
Browse files Browse the repository at this point in the history
Resolves #25
  • Loading branch information
JakeStanger committed Nov 2, 2023
1 parent 233bf74 commit 91b8d5c
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 21 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions assets/inputs/quoted_keys.corn
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
'foo.bar' = 42
'green.eggs'.and.ham = "hello world"
'with spaces' = true
'escaped\'quote' = false
'escaped=equals' = -3
}
10 changes: 10 additions & 0 deletions assets/outputs/json/quoted_keys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"foo.bar": 42,
"green.eggs": {
"and": {
"ham": "hello world"
}
},
"with spaces": true,
"escaped'quote": false
}
7 changes: 7 additions & 0 deletions assets/outputs/toml/quoted_keys.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"foo.bar" = 42
"with spaces" = true
"escaped'quote" = false

["green.eggs".and]
ham = "hello world"

7 changes: 7 additions & 0 deletions assets/outputs/yaml/quoted_keys.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
foo.bar: 42
green.eggs:
and:
ham: hello world
with spaces: true
escaped'quote: false

5 changes: 2 additions & 3 deletions libcorn/src/de.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::collections::VecDeque;

use serde::de::{self, DeserializeSeed, EnumAccess, IntoDeserializer, VariantAccess, Visitor};
Expand Down Expand Up @@ -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!(),
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 13 additions & 2 deletions libcorn/src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
5 changes: 4 additions & 1 deletion libcorn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cow<'a, str>, 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<Value<'a>>),
/// UTF-8 string
Expand Down
43 changes: 29 additions & 14 deletions libcorn/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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<IndexMap<&'a str, Value<'a>>> {
fn parse_object(&self, block: Pair<'a, Rule>) -> Result<Object<'a>> {
assert_eq!(block.as_rule(), Rule::object);

let mut obj = IndexMap::new();
Expand All @@ -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::<Vec<_>>().as_slice(),
value,
)?;
obj = Self::add_at_path(obj, &paths, value)?;
}
Rule::spread => {
let input = pair
Expand All @@ -235,23 +234,39 @@ impl<'a> CornParser<'a> {
Ok(obj)
}

fn parse_path(path: Pair<Rule>) -> Vec<Cow<str>> {
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::<Vec<_>>()
}

/// Adds `Value` at the `path` in `obj`.
///
/// `path` is an array where each entry represents another object key,
/// for example `foo.bar` is represented as `["foo", "bar"]`.
///
/// 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<IndexMap<&'a str, Value<'a>>> {
) -> Result<Object<'a>> {
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);
}

Expand All @@ -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)?),
);

Expand Down

0 comments on commit 91b8d5c

Please sign in to comment.