Skip to content

Commit

Permalink
feat(parser): add escape char, interpolation support to strings
Browse files Browse the repository at this point in the history
Escape characters like `\n`, `\r`, `\t` and unicode characters with `\u0061` are now supported in strings.

Interpolating inputs inside strings like "$foo" is now supported.
  • Loading branch information
JakeStanger committed Jun 15, 2023
1 parent 301ceea commit 53afac7
Show file tree
Hide file tree
Showing 21 changed files with 563 additions and 63 deletions.
445 changes: 440 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions assets/inputs/string.corn
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
foo = "bar"
bar = "\"\\\n\r\t"
baz = "\u0061"
qux = ""
}
6 changes: 6 additions & 0 deletions assets/inputs/string_interpolation.corn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let {
$greeting = "hello"
$subject = "world"
} in {
foo = "$greeting, $subject"
}
2 changes: 1 addition & 1 deletion assets/outputs/json/complex.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"gender": "M",
"name": {
"first": "John",
"full": "$firstName $lastName",
"full": "John Smith",
"last": "Smith"
},
"negative": {
Expand Down
5 changes: 4 additions & 1 deletion assets/outputs/json/string.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"foo": "bar"
"bar": "\"\\\n\r\t",
"baz": "a",
"foo": "bar",
"qux": ""
}
3 changes: 3 additions & 0 deletions assets/outputs/json/string_interpolation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "hello, world"
}
2 changes: 1 addition & 1 deletion assets/outputs/toml/complex.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ sinceYear = 2019

[name]
first = "John"
full = "$firstName $lastName"
full = "John Smith"
last = "Smith"

[negative]
Expand Down
5 changes: 5 additions & 0 deletions assets/outputs/toml/string.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
bar = """
\"\\
\r\t"""
baz = "a"
foo = "bar"
qux = ""

2 changes: 2 additions & 0 deletions assets/outputs/toml/string_interpolation.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo = "hello, world"

2 changes: 1 addition & 1 deletion assets/outputs/yaml/complex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ favourites:
gender: M
name:
first: John
full: $firstName $lastName
full: John Smith
last: Smith
negative:
float: -34.34
Expand Down
3 changes: 3 additions & 0 deletions assets/outputs/yaml/string.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
bar: "\"\\\n\r\t"
baz: a
foo: bar
qux: ''

2 changes: 2 additions & 0 deletions assets/outputs/yaml/string_interpolation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo: hello, world

1 change: 1 addition & 0 deletions corn-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ impl ExitCode for CornError {
CornError::InputResolveError(_) => 2,
CornError::InvalidPathError(_) => 6,
CornError::InvalidSpreadError(_) => 7,
CornError::InvalidInterpolationError(_) => 8,
CornError::DeserializationError(_) => 5,
}
}
Expand Down
3 changes: 2 additions & 1 deletion libcorn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cfg-if = "1.0.0"
serde = { version = "1.0.133", features = ["derive"] }
thiserror = "1.0.40"
wasm-bindgen = { version = "0.2.83", optional = true }
serde-wasm-bindgen = { version = "0.4.5", optional = true }
serde-wasm-bindgen = { version = "0.5.0", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
wee_alloc = { version = "0.4.5", optional = true }

Expand All @@ -30,4 +30,5 @@ wasm-bindgen-test = { version = "0.3.29" }
# required for testing
serde_json = "1.0.75"
serde_yaml = "0.9.11"
serde_bytes = "0.11.9"
toml = "0.7.4"
31 changes: 13 additions & 18 deletions libcorn/src/de.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::borrow::Cow;
use std::collections::VecDeque;

use serde::de::{DeserializeSeed, EnumAccess, IntoDeserializer, VariantAccess, Visitor};
use serde::{de, Deserialize};
use serde::de::{self, DeserializeSeed, EnumAccess, IntoDeserializer, VariantAccess, Visitor};

use crate::error::{Error, Result};
use crate::parse;
Expand Down Expand Up @@ -29,9 +29,9 @@ impl<'de> Deserializer<'de> {
/// # Errors
///
/// Will return a `DeserializationError` if the config is invalid.
pub fn from_str<'de, T>(s: &'de str) -> Result<T>
pub fn from_str<T>(s: &str) -> Result<T>
where
T: Deserialize<'de>,
T: de::DeserializeOwned,
{
let mut deserializer = Deserializer::from_str(s)?;
T::deserialize(&mut deserializer)
Expand All @@ -42,9 +42,9 @@ where
/// # Errors
///
/// Will return a `DeserializationError` if the config is invalid.
pub fn from_slice<'de, T>(bytes: &'de [u8]) -> Result<T>
pub fn from_slice<T>(bytes: &[u8]) -> Result<T>
where
T: de::Deserialize<'de>,
T: de::DeserializeOwned,
{
match std::str::from_utf8(bytes) {
Ok(s) => from_str(s),
Expand Down Expand Up @@ -99,8 +99,7 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
let seq = Seq::new(value);
visitor.visit_seq(seq)
}
Value::String(val) => visitor.visit_borrowed_str(val),
Value::EnvString(val) => visitor.visit_string(val),
Value::String(val) => visitor.visit_str(&val),
Value::Integer(val) => visitor.visit_i64(val),
Value::Float(val) => visitor.visit_f64(val),
Value::Boolean(val) => visitor.visit_bool(val),
Expand Down Expand Up @@ -192,7 +191,6 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
let value = get_value!(self);
let char = match value {
Value::String(value) => value.chars().next(),
Value::EnvString(value) => value.chars().next(),
_ => return err_expected!("char", value),
};

Expand All @@ -207,8 +205,7 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
V: Visitor<'de>,
{
match_value!(self, "string",
Value::String(val) => visitor.visit_borrowed_str(val)
Value::EnvString(val) => visitor.visit_string(val)
Value::String(val) => visitor.visit_str(&val)
)
}

Expand All @@ -224,8 +221,7 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
V: Visitor<'de>,
{
match_value!(self, "bytes array",
Value::String(val) => visitor.visit_borrowed_bytes(val.as_bytes())
Value::EnvString(val) => visitor.visit_bytes(val.as_bytes())
Value::String(val) => visitor.visit_bytes(val.as_bytes())
)
}

Expand Down Expand Up @@ -346,7 +342,6 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
match value {
Value::Object(_) => visitor.visit_enum(Enum::new(value)),
Value::String(val) => visitor.visit_enum(val.into_deserializer()),
Value::EnvString(val) => visitor.visit_enum(val.into_deserializer()),
_ => err_expected!("object or string (enum variant)", value),
}
}
Expand Down Expand Up @@ -376,7 +371,7 @@ impl<'de> Map<'de> {
Value::Object(values) => Self {
values: values
.into_iter()
.flat_map(|(key, value)| vec![Value::String(key), value])
.flat_map(|(key, value)| vec![Value::String(Cow::Borrowed(key)), value])
.collect(),
},
_ => unreachable!(),
Expand Down Expand Up @@ -473,15 +468,15 @@ impl<'de> EnumAccess<'de> for Enum<'de> {
V: DeserializeSeed<'de>,
{
match self.value {
Value::String(_) | Value::EnvString(_) => {
Value::String(_) => {
let value = seed.deserialize(&mut Deserializer::from_value(self.value))?;
Ok((value, Variant::new(None)))
}
Value::Object(obj) => {
let first_pair = obj.into_iter().next();
if let Some(first_pair) = first_pair {
let tag = seed
.deserialize(&mut Deserializer::from_value(Value::String(first_pair.0)))?;
let value = Value::String(Cow::Borrowed(first_pair.0));
let tag = seed.deserialize(&mut Deserializer::from_value(value))?;
Ok((tag, Variant::new(Some(first_pair.1))))
} else {
Err(Error::DeserializationError(
Expand Down
3 changes: 3 additions & 0 deletions libcorn/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub enum Error {
#[error("attempted to spread a type that differs from its containing type")]
InvalidSpreadError(String),

#[error("attempted to interpolate a non-string type into a string")]
InvalidInterpolationError(String),

#[error("failed to deserialize input")]
DeserializationError(String),
}
Expand Down
5 changes: 2 additions & 3 deletions libcorn/src/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@ string = ${
"\"" ~ string_val ~ "\""
}

string_val = ${ char* }
string_val = ${ (input | char)* }

char = {
// input
!("\"" | "\\") ~ ANY
| "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t")
| "\\" ~ ("\"" | "\\" | "n" | "r" | "t")
| "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4})
}

Expand Down
8 changes: 3 additions & 5 deletions libcorn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use serde::Serialize;
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::fmt::{Display, Formatter};

Expand All @@ -23,10 +24,8 @@ pub enum Value<'a> {
Object(BTreeMap<&'a str, Value<'a>>),
/// Array of values, can be mixed types.
Array(Vec<Value<'a>>),
/// Borrowed string, from string literal or input.
String(&'a str),
/// Owned string, originating from an environment variable.
EnvString(String),
/// UTF-8 string
String(Cow<'a, str>),
/// 64-bit signed integer.
Integer(i64),
/// 64-bit (double precision) floating point number.
Expand All @@ -46,7 +45,6 @@ impl<'a> Display for Value<'a> {
Value::Object(_) => "object",
Value::Array(_) => "array",
Value::String(_) => "string",
Value::EnvString(_) => "string (from environment variable)",
Value::Integer(_) => "integer",
Value::Float(_) => "float",
Value::Boolean(_) => "boolean",
Expand Down
58 changes: 53 additions & 5 deletions libcorn/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::env::var;
use std::fmt::Formatter;
Expand Down Expand Up @@ -46,7 +47,7 @@ impl<'a> CornParser<'a> {
match pair.as_rule() {
Rule::object => Ok(Value::Object(self.parse_object(pair)?)),
Rule::array => Ok(Value::Array(self.parse_array(pair)?)),
Rule::string => Ok(Value::String(Self::parse_string(pair))),
Rule::string => Ok(Value::String(self.parse_string(pair)?)),
Rule::integer => Ok(Value::Integer(Self::parse_integer(pair))),
Rule::float => Ok(Value::Float(Self::parse_float(&pair))),
Rule::boolean => Ok(Value::Boolean(Self::parse_bool(&pair))),
Expand Down Expand Up @@ -96,12 +97,59 @@ impl<'a> CornParser<'a> {

/// Collects each `char` in a `Rule::string`
/// to form a single `String`.
fn parse_string(pair: Pair<'a, Rule>) -> &'a str {
fn parse_string(&self, pair: Pair<'a, Rule>) -> Result<Cow<'a, str>> {
assert_eq!(pair.as_rule(), Rule::string);
pair.into_inner()

let mut full_string = String::new();

let pairs = pair
.into_inner()
.next()
.expect("string rules should contain a valid string value")
.as_str()
.into_inner();

for pair in pairs {
match pair.as_rule() {
Rule::char => full_string.push(Self::parse_char(&pair)),
Rule::input => {
let input_name = pair.as_str();
let value = self.get_input(input_name)?;
match value {
Value::String(value) => full_string.push_str(&value),
_ => return Err(Error::InvalidInterpolationError(input_name.to_string())),
}
}
_ => unreachable!(),
};
}

Ok(Cow::Owned(full_string))
}

fn parse_char(pair: &Pair<'a, Rule>) -> char {
let str = pair.as_str();
let mut chars = str.chars();

let first_char = chars.next().expect("character to exist");
if first_char != '\\' {
return first_char;
}

let second_char = chars.next().expect("character to exist");
if second_char != 'u' {
return match second_char {
'n' => '\n',
'r' => '\r',
't' => '\t',
'"' => '\"',
'\\' => '\\',
_ => unreachable!(),
};
}

let num =
u32::from_str_radix(&str[3..], 16).expect("valid hex characters to exist after \\u");
char::from_u32(num).unwrap_or('\u{FFFD}')
}

/// Parses each rule in a `Rule::array`
Expand Down Expand Up @@ -253,7 +301,7 @@ impl<'a> CornParser<'a> {
let var = var(env_name);

if let Ok(var) = var {
return Ok(Value::EnvString(var));
return Ok(Value::String(Cow::Owned(var)));
}
}

Expand Down
Loading

0 comments on commit 53afac7

Please sign in to comment.