use crate::part::{has_placeholder, unescaped, ungrouped};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{ToTokens, quote, quote_spanned};
use std::collections::HashSet;
use std::fmt::Write;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Expr, ExprLit, Lit, LitFloat, LitInt, LitStr, Token, braced, bracketed, token};
pub(crate) enum Value {
Literal(String),
Format(LitStr),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
Expression(Expr),
}
#[derive(Default)]
struct Writer {
interpolations: Vec<(String, TokenStream)>,
pending_text: String,
}
impl Parse for Value {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(token::Brace) {
let content;
braced!(content in input);
let mut keys = HashSet::new();
let mut entries = Vec::new();
while !content.is_empty() {
let key: LitStr = content.parse().map_err(|error| {
syn::Error::new(
error.span(),
"a key of a JSON object must be a string literal",
)
})?;
if !keys.insert(key.value()) {
let message = format!("the key `{}` must occur one time at most", key.value());
return Err(syn::Error::new(key.span(), message));
}
content.parse::<Token![:]>()?;
entries.push((key.value(), content.parse()?));
if content.is_empty() {
break;
}
content.parse::<Token![,]>()?;
}
return Ok(Value::Object(entries));
}
if input.peek(token::Bracket) {
let content;
bracketed!(content in input);
let items = Punctuated::<Value, Token![,]>::parse_terminated(&content)?;
return Ok(Value::Array(items.into_iter().collect()));
}
if input.peek(Token![-]) && (input.peek2(LitInt) || input.peek2(LitFloat)) {
input.parse::<Token![-]>()?;
return literal(&input.parse()?, "-");
}
match ungrouped(input.parse()?) {
Expr::Lit(ExprLit { lit, .. }) => literal(&lit, ""),
Expr::Path(path) if path.path.is_ident("null") => Ok(Value::Literal("null".to_owned())),
expression => Ok(Value::Expression(expression)),
}
}
}
impl Value {
pub(crate) fn is_const(&self) -> bool {
match self {
Value::Literal(_) => true,
Value::Format(_) | Value::Expression(_) => false,
Value::Array(items) => items.iter().all(Value::is_const),
Value::Object(entries) => entries.iter().all(|(_, value)| value.is_const()),
}
}
fn write(&self, writer: &mut Writer) {
match self {
Value::Literal(json) => writer.pending_text.push_str(json),
Value::Array(items) => {
writer.pending_text.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
writer.pending_text.push(',');
}
item.write(writer);
}
writer.pending_text.push(']');
}
Value::Object(entries) => {
writer.pending_text.push('{');
for (index, (key, value)) in entries.iter().enumerate() {
if index > 0 {
writer.pending_text.push(',');
}
writer.pending_text.push_str("ed(key));
writer.pending_text.push(':');
value.write(writer);
}
writer.pending_text.push('}');
}
Value::Format(literal) => {
let prefix = std::mem::take(&mut writer.pending_text);
let expression = quote_spanned!(literal.span()=> ::std::format!(#literal));
writer.interpolations.push((prefix, expression));
}
Value::Expression(expression) => {
let prefix = std::mem::take(&mut writer.pending_text);
writer
.interpolations
.push((prefix, expression.to_token_stream()));
}
}
}
}
impl ToTokens for Value {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut writer = Writer::default();
self.write(&mut writer);
let Writer {
interpolations,
pending_text,
} = writer;
let json = quote!(::system2::__private::Json);
let cow = quote!(::std::borrow::Cow);
if interpolations.is_empty() {
tokens.extend(quote!(#json::Raw(#cow::Borrowed(#pending_text))));
return;
}
let buffer = Ident::new("__system2_json", Span::mixed_site());
let pushes = interpolations.iter().map(|(prefix, expression)| {
quote_spanned! {expression.span()=>
#buffer.push_str(#prefix);
#buffer.push_str(&::system2::__private::to_json(&(#expression)));
}
});
tokens.extend(quote! {{
let mut #buffer = ::std::string::String::new();
#(#pushes)*
#buffer.push_str(#pending_text);
#json::Raw(#cow::Owned(#buffer))
}});
}
}
fn literal(lit: &Lit, sign: &str) -> syn::Result<Value> {
let json = match lit {
Lit::Str(text) if has_placeholder(&text.value()) => return Ok(Value::Format(text.clone())),
Lit::Str(text) => quoted(&unescaped(text)?),
Lit::Bool(value) => value.value.to_string(),
Lit::Int(number) if number.suffix().is_empty() => {
format!("{sign}{}", number.base10_digits())
}
Lit::Float(number) if number.suffix().is_empty() => {
let Some(number) = serde_json::Number::from_f64(number.base10_parse()?) else {
let message = "a JSON number must fit in an `f64`";
return Err(syn::Error::new(lit.span(), message));
};
format!("{sign}{number}")
}
Lit::Int(_) | Lit::Float(_) => {
let message = "a JSON number must have no suffix, such as `u32`";
return Err(syn::Error::new(lit.span(), message));
}
_ => {
let message = "a JSON value must be text, a number, `true`, `false`, `null`, \
an object, an array, or a variable";
return Err(syn::Error::new(lit.span(), message));
}
};
Ok(Value::Literal(json))
}
fn quoted(text: &str) -> String {
let mut json = String::from('"');
for character in text.chars() {
match character {
'"' => json.push_str("\\\""),
'\\' => json.push_str("\\\\"),
character if character < ' ' => {
_ = write!(json, "\\u{:04x}", u32::from(character));
}
character => json.push(character),
}
}
json.push('"');
json
}
#[cfg(test)]
mod tests {
use super::{Value, Writer};
use quote::{ToTokens, quote};
fn written(tokens: proc_macro2::TokenStream) -> (Vec<String>, String) {
let value: Value = syn::parse2(tokens).unwrap();
let mut writer = Writer::default();
value.write(&mut writer);
let prefixes = writer
.interpolations
.iter()
.map(|(prefix, _)| prefix.clone());
(prefixes.collect(), writer.pending_text)
}
#[test]
fn write_gives_the_json_text_of_each_literal() {
let (prefixes, text) = written(quote! {
[
null, true, false, 7, -1, 0x10, 2.5, -1., 1e3, 01.5, 1e300, 1e-7,
"a\"b\\c\nd\u{1}é", "{{}}", { "k\"{": [] }
]
});
assert_eq!(prefixes.len(), 0);
assert_eq!(
text,
r#"[null,true,false,7,-1,16,2.5,-1.0,1000.0,1.5,1e+300,1e-7,"a\"b\\c\u000ad\u0001é","{}",{"k\"{":[]}]"#
);
}
#[test]
fn write_splits_the_text_at_each_expression() {
let (prefixes, text) = written(quote!({ "a": x, "b": [-y, "{x}!"] }));
assert_eq!(prefixes, [r#"{"a":"#, r#","b":["#, ","]);
assert_eq!(text, "]}");
}
#[test]
fn to_tokens_borrows_a_literal_with_no_expressions() {
let value: Value = syn::parse2(quote!({ "a": 1 })).unwrap();
assert_eq!(
value.to_token_stream().to_string(),
quote!(::system2::__private::Json::Raw(
::std::borrow::Cow::Borrowed("{\"a\":1}")
))
.to_string()
);
}
#[test]
fn parse_refuses_a_bad_json_literal() {
let cases = [
(
quote!({ question: "x" }),
"a key of a JSON object must be a string literal",
),
(
quote!({ "a": 1, "a": 2 }),
"the key `a` must occur one time at most",
),
(
quote!({ "a": 10u32 }),
"a JSON number must have no suffix, such as `u32`",
),
(
quote!({ "a": -1.5f64 }),
"a JSON number must have no suffix, such as `u32`",
),
(
quote!({ "a": 'x' }),
"a JSON value must be text, a number, `true`, `false`, `null`, \
an object, an array, or a variable",
),
(
quote!({ "a": "a } b" }),
"each `}` in text must be written two times, as `}}`",
),
(quote!({ "a": 1e400 }), "a JSON number must fit in an `f64`"),
(quote!({ "a" 1 }), "expected `:`"),
(quote!({ "a": 1 "b": 2 }), "expected `,`"),
];
for (tokens, message) in cases {
let error = syn::parse2::<Value>(tokens.clone()).err().unwrap();
assert_eq!(error.to_string(), message, "{tokens}");
}
}
}