From 1b26e23d02a062ae182cea2c693f95fb28f4591f Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Tue, 21 Feb 2017 20:48:08 +0100 Subject: [PATCH] Implement edn pretty printing using `pretty.rs`. Fixes #195. (#245) * Implement pretty printing Signed-off-by: Victor Porof * Rewrite pretty printing. This does a few things. First, it use pretty.rs directly, without the layer of macro obfuscation. The code is significantly simpler as a result. Second, it tightens the layout, using pretty.rs to group nested layouts that fit on a single line. This is Clojure's EDN style, more or less. Third, it drops "special format" support for queries. This wasn't completely implemented; if we want it, we can newtype Query(edn::Value) and figure out how to really implement this idea. * Rename to reflect functionality. * Make write interface more Rust-like. There isn't a clear standard in the stdlib, but a function that takes ownership of a writer and then returns it back is definitely not Rust-like. That's what a (mutable) reference is for. * Review comment: Use as_ref to avoid cloning strings. * Post: Fix tests to use `without_spans()`. --- edn/Cargo.toml | 2 + edn/src/lib.rs | 7 +- edn/src/pretty_print.rs | 192 ++++++++++++++++++++++++++++++++++++++++ edn/src/types.rs | 2 + 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 edn/src/pretty_print.rs diff --git a/edn/Cargo.toml b/edn/Cargo.toml index 00750f10..aea1ed00 100644 --- a/edn/Cargo.toml +++ b/edn/Cargo.toml @@ -11,8 +11,10 @@ build = "build.rs" readme = "./README.md" [dependencies] +itertools = "0.5.9" num = "0.1.35" ordered-float = "0.4.0" +pretty = "0.2.0" [build-dependencies] peg = "0.5.1" diff --git a/edn/src/lib.rs b/edn/src/lib.rs index 61855f26..64e81758 100644 --- a/edn/src/lib.rs +++ b/edn/src/lib.rs @@ -8,13 +8,14 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -#![allow(dead_code)] - -extern crate ordered_float; +extern crate itertools; extern crate num; +extern crate ordered_float; +extern crate pretty; pub mod symbols; pub mod types; +pub mod pretty_print; pub mod utils; pub mod parse { diff --git a/edn/src/pretty_print.rs b/edn/src/pretty_print.rs new file mode 100644 index 00000000..de69a58f --- /dev/null +++ b/edn/src/pretty_print.rs @@ -0,0 +1,192 @@ +// Copyright 2016 Mozilla +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +use itertools::Itertools; +use pretty; + +use std::io; +use std::borrow::Cow; + +use types::Value; + +impl Value { + /// Return a pretty string representation of this `Value`. + pub fn to_pretty(&self, width: usize) -> Result { + let mut out = Vec::new(); + self.write_pretty(width, &mut out)?; + Ok(String::from_utf8_lossy(&out).into_owned()) + } + + /// Write a pretty representation of this `Value` to the given writer. + pub fn write_pretty(&self, width: usize, out: &mut W) -> Result<(), io::Error> where W: io::Write { + self.as_doc(&pretty::BoxAllocator).1.render(width, out) + } + + /// Bracket a collection of values. + /// + /// We aim for + /// [1 2 3] + /// and fall back if necessary to + /// [1, + /// 2, + /// 3]. + fn bracket<'a, A, T, I>(&'a self, allocator: &'a A, open: T, vs: I, close: T) -> pretty::DocBuilder<'a, A> + where A: pretty::DocAllocator<'a>, T: Into>, + I: IntoIterator, + { + let open = open.into(); + let n = open.len(); + let i = vs.into_iter().map(|ref v| v.as_doc(allocator)).intersperse(allocator.space()); + allocator.text(open) + .append(allocator.concat(i).nest(n)) + .append(allocator.text(close)) + .group() + } + + /// Recursively traverses this value and creates a pretty.rs document. + /// This pretty printing implementation is optimized for edn queries + /// readability and limited whitespace expansion. + pub fn as_doc<'a, A>(&'a self, pp: &'a A) -> pretty::DocBuilder<'a, A> + where A: pretty::DocAllocator<'a> { + match self { + &Value::Vector(ref vs) => self.bracket(pp, "[", vs, "]"), + &Value::List(ref vs) => self.bracket(pp, "(", vs, ")"), + &Value::Set(ref vs) => self.bracket(pp, "#{", vs, "}"), + &Value::Map(ref vs) => { + let xs = vs.iter().rev().map(|(ref k, ref v)| k.as_doc(pp).append(pp.space()).append(v.as_doc(pp)).group()).intersperse(pp.space()); + pp.text("{") + .append(pp.concat(xs).nest(1)) + .append(pp.text("}")) + .group() + } + &Value::NamespacedSymbol(ref v) => pp.text(v.namespace.as_ref()).append("/").append(v.name.as_ref()), + &Value::PlainSymbol(ref v) => pp.text(v.0.as_ref()), + &Value::NamespacedKeyword(ref v) => pp.text(":").append(v.namespace.as_ref()).append("/").append(v.name.as_ref()), + &Value::Keyword(ref v) => pp.text(":").append(v.0.as_ref()), + &Value::Text(ref v) => pp.text("\"").append(v.as_ref()).append("\""), + _ => pp.text(self.to_string()) + } + } +} + +#[cfg(test)] +mod test { + use parse; + + #[test] + fn test_pp_io() { + let string = "$"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.write_pretty(40, &mut Vec::new()).is_ok(), true); + } + + #[test] + fn test_pp_types_empty() { + let string = "[ [ ] ( ) #{ } { }, \"\" ]"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.to_pretty(40).unwrap(), "[[] () #{} {} \"\"]"); + } + + #[test] + fn test_vector() { + let string = "[1 2 3 4 5 6]"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.to_pretty(20).unwrap(), "[1 2 3 4 5 6]"); + assert_eq!(data.to_pretty(10).unwrap(), "\ +[1 + 2 + 3 + 4 + 5 + 6]"); + } + + #[test] + fn test_map() { + let string = "{:a 1 :b 2 :c 3}"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.to_pretty(20).unwrap(), "{:a 1 :b 2 :c 3}"); + assert_eq!(data.to_pretty(10).unwrap(), "\ +{:a 1 + :b 2 + :c 3}"); + } + + #[test] + fn test_pp_types() { + let string = "[ 1 2 ( 3.14 ) #{ 4N } { foo/bar 42 :baz/boz 43 } [ ] :five :six/seven eight nine/ten true false nil #f NaN #f -Infinity #f +Infinity ]"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.to_pretty(40).unwrap(), "\ +[1 + 2 + (3.14) + #{4N} + {:baz/boz 43 foo/bar 42} + [] + :five + :six/seven + eight + nine/ten + true + false + nil + #f NaN + #f -Infinity + #f +Infinity]"); + } + + #[test] + fn test_pp_query1() { + let string = "[:find ?id ?bar ?baz :in $ :where [?id :session/keyword-foo ?symbol1 ?symbol2 \"some string\"] [?tx :db/tx ?ts]]"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.to_pretty(40).unwrap(), "\ +[:find + ?id + ?bar + ?baz + :in + $ + :where + [?id + :session/keyword-foo + ?symbol1 + ?symbol2 + \"some string\"] + [?tx :db/tx ?ts]]"); + } + + #[test] + fn test_pp_query2() { + let string = "[:find [?id ?bar ?baz] :in [$] :where [?id :session/keyword-foo ?symbol1 ?symbol2 \"some string\"] [?tx :db/tx ?ts] (not-join [?id] [?id :session/keyword-bar _])]"; + let data = parse::value(string).unwrap().without_spans(); + + assert_eq!(data.to_pretty(40).unwrap(), "\ +[:find + [?id ?bar ?baz] + :in + [$] + :where + [?id + :session/keyword-foo + ?symbol1 + ?symbol2 + \"some string\"] + [?tx :db/tx ?ts] + (not-join + [?id] + [?id :session/keyword-bar _])]"); + } +} diff --git a/edn/src/types.rs b/edn/src/types.rs index b2a5d5e5..cfcfafd3 100644 --- a/edn/src/types.rs +++ b/edn/src/types.rs @@ -8,6 +8,8 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +#![cfg_attr(feature = "cargo-clippy", allow(linkedlist))] + use std::collections::{BTreeSet, BTreeMap, LinkedList}; use std::cmp::{Ordering, Ord, PartialOrd}; use std::fmt::{Display, Formatter};