json_str 0.5.2

Write json literals without ugly strings.
Documentation
extern crate syntax;
extern crate rustc;
extern crate rustc_plugin;

use std::collections::BTreeMap;
use rustc_plugin::Registry;
use syntax::codemap::Span;
use syntax::print::pprust::tts_to_string;
use syntax::parse::token::{Token, BinOpToken};
use syntax::symbol::Symbol;
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ ExtCtxt, MacResult, MacEager, DummyResult };
use syntax::ext::build::AstBuilder;
use syntax::ast::*;

use ::parse::*;

fn tts_to_json(tts: &[TokenTree]) -> String {
    let json_raw = tts_to_string(&tts);
    let mut sanitised = String::with_capacity(json_raw.len());

    parse_literal(json_raw.as_bytes(), &mut sanitised);

    sanitised
}

//Parse a token tree to a json `str` at compile time.
pub fn expand_json_lit(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box<MacResult+'static> {
    let json = tts_to_json(tts);

    let str_lit = cx.expr_str(sp, Symbol::intern(&json));
    MacEager::expr(quote_expr!(cx, $str_lit))
}

//Parse a token tree to a json `String` at compile time.
pub fn expand_json_string(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box<MacResult+'static> {
    let json = tts_to_json(tts);

    let str_lit = cx.expr_str(sp, Symbol::intern(&json));
    MacEager::expr(quote_expr!(cx, String::from($str_lit)))
}

//Parse a token tree to a closure at compile time.
//We can't just parse to a Rust closure because the syntax isn't valid.
//The `|arg1, arg2|` needs to be parsed independently of the rest.
pub fn expand_json_fn(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box<MacResult+'static> {
    let mut parser = cx.new_parser_from_tts(tts);

    // Expect an opening '|'
    if !parser.eat(&Token::BinOp(BinOpToken::Or)) {
        cx.span_err(sp, &format!("expected single '|', found '{}'", parser.this_token_to_string()));
        return DummyResult::any(sp);
    }

    let mut ordered_args = Vec::new();
    let mut repl_args = BTreeMap::<String, Ident>::new();

    loop {
        // If we encounter a '|' then stop parsing args
        if parser.eat(&Token::BinOp(BinOpToken::Or)) {
            break;
        }
        // Otherwise parse a function argument
        else {
            let arg = match parser.parse_fn_block_arg() {
                Ok(arg) => arg,
                Err(mut e) => {
                    e.emit();
                    return DummyResult::any(sp);
                }
            };

            match arg.pat.node {
                PatKind::Ident(_, ident, _) => {
                    let key = ident.name.as_str().to_string();
                    
                    repl_args.insert(key, ident);
                    ordered_args.push(ident);
                },
                _ => {
                    cx.span_err(sp, "expected ident");
                    return DummyResult::any(sp);
                }
            }
        }

        // Eat any commas
        parser.eat(&Token::Comma);
    }

    // Treat the rest of the token stream as json
    let json_tts = match parser.parse_all_token_trees() {
        Ok(tts) => tts,
        Err(mut e) => {
            e.emit();
            return DummyResult::any(sp);
        }
    };

    // Parse the token tree to json fragments
    let json_raw = tts_to_string(&json_tts);
    let mut fragments = Vec::new();

    parse_fragments(json_raw.as_bytes(), &mut fragments);

    let mut stmts = Vec::new();

    let result_ident = cx.ident_of("result");
    let let_stmt = quote_stmt!(cx, let mut $result_ident = String::new();).unwrap();
    let ret_stmt = quote_stmt!(cx, $result_ident).unwrap();

    stmts.push(let_stmt);

    // Create an expression to push each fragment
    for fragment in fragments {
        match fragment {
            JsonFragment::Literal(json) => {
                let str_lit = cx.expr_str(sp, Symbol::intern(&json));
                let push_stmt = quote_stmt!(cx, $result_ident.push_str($str_lit);).unwrap();

                stmts.push(push_stmt);
            },
            JsonFragment::Repl(repl) => {
                let ident = match repl_args.get(repl) {
                    Some(ident) => ident,
                    _ => {
                        cx.span_err(sp, &format!("replacement '{}' is not in the list of fn args", repl));
                        return DummyResult::any(sp);
                    }
                };

                let push_stmt = quote_stmt!(cx, $result_ident.push_str($ident);).unwrap();

                stmts.push(push_stmt);
            }
        }
    }

    stmts.push(ret_stmt);

    let block = cx.block(sp, stmts);
    let lambda_expr = cx.lambda(sp, ordered_args, quote_expr!(cx, $block));

    MacEager::expr(lambda_expr)
}

#[doc(hidden)]
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("json_lit", expand_json_lit);
    reg.register_macro("json_str", expand_json_string);
    reg.register_macro("json_fn", expand_json_fn);
}