Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ability to quote keys to escape limitations #27

Merged
merged 1 commit into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading