Skip to content
New issue

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

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

Already on GitHub? # to your account

♻️ Prep for spec string changes #23

Merged
merged 2 commits into from
Nov 30, 2024
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
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ let package = match manifest.get("package").unwrap() {
Value::Table(package) => package,
_ => panic!(),
};
assert_eq!(package.get("name").unwrap(), &Value::String("example"));
assert_eq!(package.get("version").unwrap(), &Value::String("0.1.0"));
assert_eq!(package.get("edition").unwrap(), &Value::String("2021"));
assert_eq!(package.get("resolver").unwrap(), &Value::String("2"));
assert_eq!(package.get("name").unwrap(), &Value::String("example".into()));
assert_eq!(package.get("version").unwrap(), &Value::String("0.1.0".into()));
assert_eq!(package.get("edition").unwrap(), &Value::String("2021".into()));
assert_eq!(package.get("resolver").unwrap(), &Value::String("2".into()));

let deps = match manifest.get("dependencies").unwrap() {
Value::Table(deps) => deps,
Expand All @@ -88,17 +88,17 @@ let serde = match deps.get("serde").unwrap() {
Value::Table(serde) => serde,
_ => panic!(),
};
assert_eq!(serde.get("version").unwrap(), &Value::String("1.0"));
assert_eq!(serde.get("version").unwrap(), &Value::String("1.0".into()));
let serde_features = match serde.get("features").unwrap() {
Value::Array(features) => features.as_slice(),
_ => panic!(),
};
assert_eq!(serde_features, &[Value::String("std"), Value::String("derive")]);
assert_eq!(serde_features, &[Value::String("std".into()), Value::String("derive".into())]);
let regex = match deps.get("regex").unwrap() {
Value::String(regex) => *regex,
Value::String(regex) => regex,
_ => panic!(),
};
assert_eq!(regex, "1.5");
assert_eq!(&*regex, "1.5");

const CARGO_TOML: &'static str = r#"
[package]
Expand Down
92 changes: 22 additions & 70 deletions src/parse/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
mod ignored;
mod numbers;
mod strings;

use crate::{Array, Error, ParseError, Table, Value};

use alloc::{vec, vec::Vec};
use alloc::{borrow::Cow, vec, vec::Vec};
use ignored::{parse_comment_newline, parse_whitespace_n_comments};
use winnow::{
ascii::{multispace1, space0},
combinator::{alt, cut_err, delimited, opt, peek, preceded, repeat, separated, separated_pair},
error::ContextError,
token::{take_until, take_while},
token::take_while,
PResult, Parser,
};

Expand All @@ -35,8 +36,10 @@ pub fn parse(input: &str) -> Result<Table<'_>, Error> {
if let Some((header, is_array)) = header {
if is_array {
// Handle array of tables ([[table]])
let key = *header.last().expect("Header should not be empty");
let entry = map.entry(key).or_insert_with(|| Value::Array(Array::new()));
let key = header.last().expect("Header should not be empty").clone();
let entry = map
.entry(key.clone())
.or_insert_with(|| Value::Array(Array::new()));
if let Value::Array(array) = entry {
// Append a new empty table to the array
let new_table = Table::new();
Expand All @@ -51,7 +54,7 @@ pub fn parse(input: &str) -> Result<Table<'_>, Error> {
}
} else if !keys.is_empty() {
if let Some(ref table) = current_table {
if let Some(Value::Array(array)) = map.get_mut(table[0]) {
if let Some(Value::Array(array)) = map.get_mut(&table[0]) {
// Insert into the most recent table in the array
if let Some(Value::Table(last_table)) = array.last_mut() {
insert_nested_key(last_table, &keys, value);
Expand All @@ -77,7 +80,7 @@ pub fn parse(input: &str) -> Result<Table<'_>, Error> {
}

/// Parses a table header (e.g., `[dependencies]`)
fn parse_table_header<'i>(input: &mut &'i str) -> PResult<(Vec<&'i str>, bool), ContextError> {
fn parse_table_header<'i>(input: &mut &'i str) -> PResult<(Vec<Cow<'i, str>>, bool), ContextError> {
alt((
delimited("[[", parse_dotted_key, "]]").map(|keys| (keys, true)), // Array of tables
delimited('[', parse_dotted_key, ']').map(|keys| (keys, false)), // Regular table
Expand All @@ -86,28 +89,30 @@ fn parse_table_header<'i>(input: &mut &'i str) -> PResult<(Vec<&'i str>, bool),
}

/// Parses a single key-value pair
fn parse_key_value<'i>(input: &mut &'i str) -> PResult<(Vec<&'i str>, Value<'i>), ContextError> {
fn parse_key_value<'i>(
input: &mut &'i str,
) -> PResult<(Vec<Cow<'i, str>>, Value<'i>), ContextError> {
separated_pair(parse_dotted_key, '=', parse_value).parse_next(input)
}

/// Parses a dotted or single key
fn parse_dotted_key<'i>(input: &mut &'i str) -> PResult<Vec<&'i str>, ContextError> {
fn parse_dotted_key<'i>(input: &mut &'i str) -> PResult<Vec<Cow<'i, str>>, ContextError> {
separated(1.., parse_key, '.').parse_next(input)
}

/// Parses a key (alphanumeric or underscores)
fn parse_key<'i>(input: &mut &'i str) -> PResult<&'i str, ContextError> {
// We don't use `parse_string` here beecause in the future that will also accept multiline
fn parse_key<'i>(input: &mut &'i str) -> PResult<Cow<'i, str>, ContextError> {
// We don't use `strings::parse` here because in the future that will also accept multiline
// strings and we don't want that here.
let string_key = alt((parse_basic_string, parse_literal_string)).map(|s| match s {
let string_key = alt((strings::parse_basic, strings::parse_literal)).map(|s| match s {
Value::String(s) => s,
_ => unreachable!(),
});
delimited(
space0,
alt((
string_key,
take_while(1.., |c: char| c.is_alphanumeric() || c == '_' || c == '-'),
take_while(1.., |c: char| c.is_alphanumeric() || c == '_' || c == '-').map(Into::into),
)),
space0,
)
Expand All @@ -120,7 +125,7 @@ fn parse_value<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
space0,
// FIXME: Use `dispatch!` to make it more efficient.
alt((
parse_string,
strings::parse,
parse_float,
parse_integer,
parse_boolean,
Expand All @@ -132,59 +137,6 @@ fn parse_value<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
.parse_next(input)
}

/// Parses a string value enclosed in quotes
fn parse_string<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
// TODO:
// * Handle multiline basic and literal strings.
// * Handle escape sequences.
alt((
parse_multiline_basic_string,
parse_basic_string,
parse_multiline_literal_string,
parse_literal_string,
))
.parse_next(input)
}

/// Parses a basic string value enclosed in quotes.
fn parse_basic_string<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited('"', take_until(0.., '"'), '"')
.map(Value::String)
.parse_next(input)
}

/// Parses a literal string value enclosed in single quotes.
fn parse_literal_string<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited('\'', take_until(0.., '\''), '\'')
.map(Value::String)
.parse_next(input)
}

/// Parses a multiline basic string value enclosed in triple quotes.
fn parse_multiline_basic_string<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited(
"\"\"\"",
take_until(0.., "\"\"\"").map(|s: &str| {
// Trim leading newlines.
s.trim_start_matches('\n')
}),
"\"\"\"",
)
.map(Value::String)
.parse_next(input)
}

/// Parses a literal multiline string value enclosed in triple single quotes (`'''`).
fn parse_multiline_literal_string<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited(
"'''",
take_until(0.., "'''").map(|s: &str| s.trim_start_matches('\n')), // Trim leading newlines
"'''",
)
.map(Value::String)
.parse_next(input)
}

/// Parses an integer value
fn parse_integer<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
numbers::integer(input).map(Value::Integer)
Expand Down Expand Up @@ -236,18 +188,18 @@ fn parse_inline_table<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextErro
separated(0.., separated_pair(parse_key, '=', parse_value), ','),
'}',
)
.map(|pairs: Vec<(&'i str, Value<'i>)>| Value::Table(pairs.into_iter().collect()))
.map(|pairs: Vec<(Cow<'i, str>, Value<'i>)>| Value::Table(pairs.into_iter().collect()))
.parse_next(input)
}

/// Inserts a value into a nested map using a dotted key
fn insert_nested_key<'a>(map: &mut Table<'a>, keys: &[&'a str], value: Value<'a>) {
fn insert_nested_key<'a>(map: &mut Table<'a>, keys: &[Cow<'a, str>], value: Value<'a>) {
if let Some((first, rest)) = keys.split_first() {
if rest.is_empty() {
map.insert(first, value);
map.insert(first.clone(), value);
} else {
let entry = map
.entry(first)
.entry(first.clone())
.or_insert_with(|| Value::Table(Table::new()));

if let Value::Table(ref mut nested_map) = entry {
Expand Down
60 changes: 60 additions & 0 deletions src/parse/strings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::Value;

use winnow::{
combinator::{alt, delimited},
error::ContextError,
token::take_until,
PResult, Parser,
};

/// Parses a string value enclosed in quotes
pub(crate) fn parse<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
// TODO:
// * Handle escape sequences.
alt((
parse_multiline_basic,
parse_basic,
parse_multiline_literal,
parse_literal,
))
.parse_next(input)
}

/// Parses a basic string value enclosed in quotes.
pub(crate) fn parse_basic<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited('"', take_until(0.., '"'), '"')
.map(|s: &str| Value::String(s.into()))
.parse_next(input)
}

/// Parses a literal string value enclosed in single quotes.
pub(crate) fn parse_literal<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited('\'', take_until(0.., '\''), '\'')
.map(|s: &str| Value::String(s.into()))
.parse_next(input)
}

/// Parses a multiline basic string value enclosed in triple quotes.
pub(crate) fn parse_multiline_basic<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited(
"\"\"\"",
take_until(0.., "\"\"\"").map(|s: &str| {
// Trim leading newlines.
s.trim_start_matches('\n')
}),
"\"\"\"",
)
.map(|s| Value::String(s.into()))
.parse_next(input)
}

/// Parses a literal multiline string value enclosed in triple single quotes (`'''`).
pub(crate) fn parse_multiline_literal<'i>(input: &mut &'i str) -> PResult<Value<'i>, ContextError> {
delimited(
"'''",
take_until(0.., "'''").map(|s: &str| s.trim_start_matches('\n')), // Trim leading newlines
"'''",
)
.map(|s| Value::String(s.into()))
.parse_next(input)
}
24 changes: 17 additions & 7 deletions src/serde.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::borrow::Cow;

use crate::{
array::{self, Array},
table::{self, Table},
Error, Value,
};
use serde::de::{
self, value::BorrowedStrDeserializer, DeserializeSeed, Deserializer, IntoDeserializer,
MapAccess, SeqAccess, Visitor,
self,
value::{BorrowedStrDeserializer, StrDeserializer},
DeserializeSeed, Deserializer, IntoDeserializer, MapAccess, SeqAccess, Visitor,
};

/// Deserialize a TOML document from a string. Requires the `serde` feature.
Expand All @@ -26,7 +29,8 @@ impl<'de, 'a> Deserializer<'de> for &'a Value<'de> {
V: Visitor<'de>,
{
match self {
Value::String(s) => visitor.visit_borrowed_str(s),
Value::String(Cow::Borrowed(s)) => visitor.visit_borrowed_str(s),
Value::String(Cow::Owned(s)) => visitor.visit_str(s),
Value::Integer(i) => visitor.visit_i64(*i),
Value::Float(f) => visitor.visit_f64(*f),
Value::Boolean(b) => visitor.visit_bool(*b),
Expand All @@ -40,7 +44,8 @@ impl<'de, 'a> Deserializer<'de> for &'a Value<'de> {
V: Visitor<'de>,
{
match self {
Value::String(s) => visitor.visit_borrowed_str(s),
Value::String(Cow::Borrowed(s)) => visitor.visit_borrowed_str(s),
Value::String(Cow::Owned(s)) => visitor.visit_str(s),
_ => Err(de::Error::invalid_type(
de::Unexpected::Other("non-string"),
&visitor,
Expand Down Expand Up @@ -141,7 +146,7 @@ impl<'de, 'a> Deserializer<'de> for &'a Value<'de> {
V: Visitor<'de>,
{
match self {
Value::String(s) => visitor.visit_enum(s.into_deserializer()),
Value::String(s) => visitor.visit_enum(s.clone().into_deserializer()),
// TODO: Support non-unit enums.
_ => Err(de::Error::invalid_type(
de::Unexpected::Other("non-string"),
Expand Down Expand Up @@ -203,8 +208,13 @@ impl<'de, 'a> MapAccess<'de> for MapDeserializer<'de, 'a> {
{
if let Some((key, value)) = self.iter.next() {
self.value = Some(value);
seed.deserialize((BorrowedStrDeserializer::new(key)).into_deserializer())
.map(Some)
match key {
Cow::Owned(s) => seed.deserialize(StrDeserializer::new(s).into_deserializer()),
Cow::Borrowed(s) => {
seed.deserialize(BorrowedStrDeserializer::new(s).into_deserializer())
}
}
.map(Some)
} else {
Ok(None)
}
Expand Down
Loading