diff --git a/README.md b/README.md index cc7f3e3..bb04fae 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Puget [![Build Status](https://travis-ci.org/greglook/puget.svg?branch=master)](https://travis-ci.org/greglook/puget) [![Coverage Status](https://coveralls.io/repos/greglook/puget/badge.png?branch=master)](https://coveralls.io/r/greglook/puget?branch=master) -[![Dependency Status](https://www.versioneye.com/user/projects/53718bfb14c1589a89000144/badge.png)](https://www.versioneye.com/clojure/mvxcvi:puget/0.5.1) +[![Dependency Status](https://www.versioneye.com/user/projects/53718bfb14c1589a89000144/badge.png)](https://www.versioneye.com/clojure/mvxcvi:puget/0.5.2) Puget is a Clojure library for printing [EDN](https://github.com/edn-format/edn) values. Under the hood, Puget formats data into a _print document_ and uses the @@ -20,7 +20,7 @@ To use this version with Leiningen, add the following dependency to your project definition: ```clojure -[mvxcvi/puget "0.5.1"] +[mvxcvi/puget "0.5.2"] ``` ## Syntax Coloring diff --git a/project.clj b/project.clj index 9510c37..13a0039 100644 --- a/project.clj +++ b/project.clj @@ -1,13 +1,15 @@ -(defproject mvxcvi/puget "0.5.1" +(defproject mvxcvi/puget "0.5.2" :description "Colorizing canonical Clojure printer for EDN values." :url "https://github.com/greglook/puget" :license {:name "Public Domain" :url "http://unlicense.org/"} + :deploy-branches ["master"] + :dependencies [[org.clojure/clojure "1.6.0"] [org.clojure/data.codec "0.1.0"] - [fipp "0.4.1"]] + [fipp "0.4.2"]] :profiles {:coverage diff --git a/src/puget/data.clj b/src/puget/data.clj index d7f4e35..7bfc866 100644 --- a/src/puget/data.clj +++ b/src/puget/data.clj @@ -4,95 +4,9 @@ [clojure.data.codec.base64 :as b64]) (:import (java.net URI) - (java.text SimpleDateFormat) (java.util Date TimeZone UUID))) -;; TOTAL-ORDERING COMPARATOR - -(defn- type-priority - "Determines the 'priority' of the given value based on its type: - - nil - - Boolean - - Number - - Character - - String - - Keyword - - Symbol - - List - - Vector - - Set - - Map - - (all other types)" - [x] - (let [predicates [nil? false? true? number? char? string? - keyword? symbol? list? vector? set? map?] - priority (->> predicates - (map vector (range)) - (some (fn [[i p]] (when (p x) i))))] - (or priority (count predicates)))) - - -(defn- compare-seqs - "Compare sequences using the given comparator. If any element of the - sequences orders differently, it determines the ordering. Otherwise, if the - prefix matches, the longer sequence sorts later." - [order xs ys] - (or (some #(when-not (= 0 %) %) - (map order xs ys)) - (- (count xs) (count ys)))) - - -(defn total-order - "Comparator function that provides a total-ordering of EDN values. Values of - different types sort in order of their types, per `type-priority`. `false` - is before `true`, numbers are ordered by magnitude regardless of type, and - characters, strings, keywords, and symbols are ordered lexically. - - Sequential collections are sorted by comparing their elements one at a time. - If the sequences have equal leading elements, the longer one is ordered later. - Sets are compared by cardinality first, then elements in sorted order. - Finally, maps are compared by their entries in sorted order of their keys. - - All other types are sorted by class name. If the class implements - `Comparable`, the instances of it are compared using `compare`. Otherwise, the - values are ordered by print representation." - [a b] - (if (= a b) 0 - (let [pri-a (type-priority a) - pri-b (type-priority b)] - (cond - (< pri-a pri-b) -1 - (> pri-a pri-b) 1 - - (some #(% a) #{number? char? string? keyword? symbol?}) - (compare a b) - - (map? a) - (compare-seqs total-order - (sort-by first total-order (seq a)) - (sort-by first total-order (seq b))) - - (set? a) - (let [size-diff (- (count a) (count b))] - (if (not= size-diff 0) - size-diff - (compare-seqs total-order a b))) - - (coll? a) - (compare-seqs total-order a b) - - :else - (let [class-diff (compare (.getName (class a)) - (.getName (class b)))] - (if (not= class-diff 0) - class-diff - (if (instance? java.lang.Comparable a) - (compare a b) - (compare (pr-str a) (pr-str b))))))))) - - - ;; TAGGED VALUE PROTOCOL (defprotocol TaggedValue @@ -153,10 +67,11 @@ (defn- format-utc "Produces an ISO-8601 formatted date-time string from the given Date." [^Date date] - (let [date-format (doto (java.text.SimpleDateFormat. - "yyyy-MM-dd'T'HH:mm:ss.SSS-00:00") - (.setTimeZone (TimeZone/getTimeZone "GMT")))] - (.format date-format date))) + (-> + "yyyy-MM-dd'T'HH:mm:ss.SSS-00:00" + java.text.SimpleDateFormat. + (doto (.setTimeZone (TimeZone/getTimeZone "GMT"))) + (.format date))) (extend-tagged-value Date 'inst format-utc) diff --git a/src/puget/order.clj b/src/puget/order.clj new file mode 100644 index 0000000..26554c4 --- /dev/null +++ b/src/puget/order.clj @@ -0,0 +1,83 @@ +(ns puget.order + "Total ordering comparator for Clojure values.") + + +(defn- type-priority + "Determines the 'priority' of the given value based on its type: + - nil + - Boolean + - Number + - Character + - String + - Keyword + - Symbol + - List + - Vector + - Set + - Map + - (all other types)" + [x] + (let [predicates [nil? false? true? number? char? string? + keyword? symbol? list? vector? set? map?] + priority (->> predicates + (map vector (range)) + (some (fn [[i p]] (when (p x) i))))] + (or priority (count predicates)))) + + +(defn- compare-seqs + "Compare sequences using the given comparator. If any element of the + sequences orders differently, it determines the ordering. Otherwise, if the + prefix matches, the longer sequence sorts later." + [order xs ys] + (or (first (remove zero? (map order xs ys))) + (- (count xs) (count ys)))) + + +(defn rank + "Comparator function that provides a total ordering of EDN values. Values of + different types sort in order of their types, per `type-priority`. `false` + is before `true`, numbers are ordered by magnitude regardless of type, and + characters, strings, keywords, and symbols are ordered lexically. + + Sequential collections are sorted by comparing their elements one at a time. + If the sequences have equal leading elements, the longer one is ordered later. + Sets are compared by cardinality first, then elements in sorted order. + Finally, maps are compared by their entries in sorted order of their keys. + + All other types are sorted by class name. If the class implements + `Comparable`, the instances of it are compared using `compare`. Otherwise, the + values are ordered by print representation." + [a b] + (if (= a b) 0 + (let [pri-a (type-priority a) + pri-b (type-priority b)] + (cond + (< pri-a pri-b) -1 + (> pri-a pri-b) 1 + + (some #(% a) #{number? char? string? keyword? symbol?}) + (compare a b) + + (map? a) + (compare-seqs rank + (sort-by first rank (seq a)) + (sort-by first rank (seq b))) + + (set? a) + (let [size-diff (- (count a) (count b))] + (if (zero? size-diff) + (compare-seqs rank a b) + size-diff)) + + (coll? a) + (compare-seqs rank a b) + + :else + (let [class-diff (compare (.getName (class a)) + (.getName (class b)))] + (if (zero? class-diff) + (if (instance? java.lang.Comparable a) + (compare a b) + (compare (pr-str a) (pr-str b))) + class-diff)))))) diff --git a/src/puget/printer.clj b/src/puget/printer.clj index f2ea275..53dbf90 100644 --- a/src/puget/printer.clj +++ b/src/puget/printer.clj @@ -2,10 +2,11 @@ "Functions for canonical colored printing of EDN values." (:require [clojure.string :as str] - [fipp.printer :refer [defprinter]] + [fipp.printer :as fipp] (puget [ansi :as ansi] - [data :as data]))) + [data :as data] + [order :as order]))) ;; CONTROL VARS @@ -151,7 +152,7 @@ (defmethod canonize clojure.lang.IPersistentSet [s] - (let [entries (sort data/total-order (seq s))] + (let [entries (sort order/rank (seq s))] [:group (color-doc :delimiter "#{") [:align (interpose :line (map canonize entries))] @@ -161,9 +162,7 @@ (defn- canonize-map [m] (let [canonize-kv (fn [[k v]] [:span (canonize k) " " (canonize v)]) - entries (->> (seq m) - (sort-by first data/total-order) - (map canonize-kv))] + entries (->> (seq m) (sort-by first order/rank) (map canonize-kv))] [:group (color-doc :delimiter "{") [:align (interpose [:span *map-delimiter* :line] entries)] @@ -214,16 +213,22 @@ ;; PRINT FUNCTIONS -(defprinter pprint canonize {:width 80}) +(def ^:private default-opts + "Default Puget printing options." + {:width 80}) + + +(defn pprint + ([value] + (pprint value default-opts)) + ([value opts] + (fipp/pprint-document (canonize value) opts))) (defn pprint-str "Pretty-print a value to a string." ([value] - (-> value - pprint - with-out-str - str/trim-newline)) + (pprint-str value default-opts)) ([value opts] (-> value (pprint opts) @@ -234,8 +239,7 @@ (defn cprint "Like pprint, but turns on colored output." ([value] - (binding [*colored-output* true] - (pprint value))) + (cprint value default-opts)) ([value opts] (binding [*colored-output* true] (pprint value opts)))) @@ -244,10 +248,7 @@ (defn cprint-str "Pretty-prints a value to a colored string." ([value] - (-> value - cprint - with-out-str - str/trim-newline)) + (cprint-str value default-opts)) ([value opts] (-> value (cprint opts) diff --git a/test/puget/data_test.clj b/test/puget/data_test.clj index 44ac4d6..73fba35 100644 --- a/test/puget/data_test.clj +++ b/test/puget/data_test.clj @@ -5,68 +5,36 @@ [puget.data :as data])) -(defn- is-sorted - [& values] - (is (= values (sort data/total-order (shuffle values))))) - - -(deftest order-primitives - (is-sorted - nil false true 0 \a "a" :a 'a)) - -(deftest order-numbers - (is-sorted - -123 0.0 3.14159M 4096N)) - -(deftest order-strings - (is-sorted - "alpha" "alphabet" "beta" "omega")) - -(deftest order-keywords - (is-sorted - :foo :zap :a-ns/baz :my-ns/bar)) - -(deftest order-symbols - (is-sorted - 'x 'y 'aaa/foo 'z/bar)) - -(deftest order-sequences - (is-sorted - '(1 2 3) [1 2 3] [1 2 3 4] [1 2 4] [1 \2 "3"] [\1] #{\1})) - -(deftest order-sets - (is-sorted - #{:one} #{:two} #{:zzz} #{:one :two} #{:one :zzz})) +(defn test-tagged-value + [data t v & [reader]] + (is (= t (data/edn-tag data))) + (is (= v (data/edn-value data))) + (let [s (data/edn-str data) + data' (edn/read-string (if reader {:readers {t reader}} {}) s)] + (is (= s (pr-str data))) + (is (= data data')))) -(deftest order-maps - (is-sorted - {:a 1 :b 2/3} {:a 1 :b 2/3 :c 'x} {:a 1 :b 4/3} {:x 1 :y 2})) -(deftest order-classes - (is-sorted - (java.util.Currency/getInstance "JPY") - (java.util.Currency/getInstance "USD") - (java.util.Date. 1234567890) - (java.util.Date. 1234567891))) +(deftest inst-tagged-values + (test-tagged-value + (java.util.Date. 1383271402749) + 'inst "2013-11-01T02:03:22.749-00:00")) -(defrecord TestRecord [x y]) -(data/extend-tagged-map TestRecord 'test/record) -(deftest tagged-value-extension - (let [rec (TestRecord. :foo :bar)] - (is (= 'test/record (data/edn-tag rec))) - (is (= {:x :foo, :y :bar} (data/edn-value rec))))) +(deftest uuid-tagged-values + (test-tagged-value + (java.util.UUID/fromString "96d91316-53b9-4800-81c1-97ae9f4b86b0") + 'uuid "96d91316-53b9-4800-81c1-97ae9f4b86b0")) -(deftest generic-tagged-value - (let [data (data/tagged-value 'foo :bar) - string (data/edn-str data)] - (is (= 'foo (data/edn-tag data))) - (is (= :bar (data/edn-value data))) - (is (= data (edn/read-string {:default data/tagged-value} string))))) +(deftest uri-tagged-values + (test-tagged-value + (java.net.URI. "http://en.wikipedia.org/wiki/Uniform_resource_identifier") + 'uri "http://en.wikipedia.org/wiki/Uniform_resource_identifier" + data/read-uri)) -(deftest byte-arrays +(deftest bin-tagged-values (let [byte-arr (.getBytes "foobarbaz") string (data/edn-str byte-arr) read-arr (edn/read-string {:readers {'bin data/read-bin}} string)] @@ -76,22 +44,18 @@ (is (= (seq byte-arr) (seq read-arr))))) -(defn test-tagged-value - [data t v] - (is (= t (data/edn-tag data))) - (is (= v (data/edn-value data))) - (let [s (data/edn-str data)] - (is (= s (pr-str data))))) +(deftest generic-tagged-value + (let [data (data/tagged-value 'foo :bar) + string (data/edn-str data)] + (is (= 'foo (data/edn-tag data))) + (is (= :bar (data/edn-value data))) + (is (= "#foo :bar" (pr-str data))) + (is (= data (edn/read-string {:default data/tagged-value} string))))) -(deftest built-in-printing - (testing "TaggedValue" - (test-tagged-value - (java.util.Date. 1383271402749) - 'inst "2013-11-01T02:03:22.749-00:00") - (test-tagged-value - (java.util.UUID/fromString "96d91316-53b9-4800-81c1-97ae9f4b86b0") - 'uuid "96d91316-53b9-4800-81c1-97ae9f4b86b0") - (test-tagged-value - (java.net.URI. "http://en.wikipedia.org/wiki/Uniform_resource_identifier") - 'uri "http://en.wikipedia.org/wiki/Uniform_resource_identifier"))) +(defrecord TestRecord [x y]) +(data/extend-tagged-map TestRecord 'test/record) +(deftest tagged-value-extension + (let [rec (TestRecord. :foo :bar)] + (is (= 'test/record (data/edn-tag rec))) + (is (= {:x :foo, :y :bar} (data/edn-value rec))))) diff --git a/test/puget/order_test.clj b/test/puget/order_test.clj new file mode 100644 index 0000000..748c2b1 --- /dev/null +++ b/test/puget/order_test.clj @@ -0,0 +1,58 @@ +(ns puget.order-test + (:require + [clojure.test :refer :all] + [puget.order :as order])) + + +(defn- is-sorted + [& values] + (dotimes [_ 10] + (is (= values (sort order/rank (shuffle values)))))) + + +(deftest primitive-ordering + (is-sorted + nil false true 0 \a "a" :a 'a)) + + +(deftest number-ordering + (is-sorted + -123 0.0 3.14159M 4096N)) + + +(deftest string-ordering + (is-sorted + "alpha" "alphabet" "beta" "omega")) + + +(deftest keyword-ordering + (is-sorted + :foo :zap :a-ns/baz :my-ns/bar)) + + +(deftest symbol-ordering + (is-sorted + 'x 'y 'aaa/foo 'z/bar)) + + +(deftest sequence-ordering + (is-sorted + '(1 2 3) [1 2 3] [1 2 3 4] [1 2 4] [1 \2 "3"] [\1] #{\1})) + + +(deftest set-ordering + (is-sorted + #{:one} #{:two} #{:zzz} #{:one :two} #{:one :zzz})) + + +(deftest map-ordering + (is-sorted + {:a 1 :b 2/3} {:a 1 :b 2/3 :c 'x} {:a 1 :b 4/3} {:x 1 :y 2})) + + +(deftest class-ordering + (is-sorted + (java.util.Currency/getInstance "JPY") + (java.util.Currency/getInstance "USD") + (java.util.Date. 1234567890) + (java.util.Date. 1234567891))) diff --git a/test/puget/printer_test.clj b/test/puget/printer_test.clj index ed1e67a..06c3e51 100644 --- a/test/puget/printer_test.clj +++ b/test/puget/printer_test.clj @@ -3,10 +3,27 @@ [clojure.string :as str] [clojure.test :refer :all] (puget - [data :as data] + [data :refer [TaggedValue]] [printer :refer :all]))) +(deftest color-scheme-setting + (let [old-scheme *color-scheme*] + (set-color-scheme! {:tag [:green]}) + (is (= [:green] (:tag *color-scheme*))) + (set-color-scheme! :nil [:black] :number [:bold :cyan]) + (is (= [:black] (:nil *color-scheme*))) + (is (= [:bold :cyan] (:number *color-scheme*))) + (set-color-scheme! old-scheme))) + + +(deftest map-delimiter-setting + (let [old-delim *map-delimiter*] + (set-map-commas!) + (is (= "," *map-delimiter*)) + (alter-var-root #'*map-delimiter* (constantly old-delim)))) + + (deftest canonical-primitives (testing "Primitive values" (are [v text] (= text (-> v pprint with-out-str str/trim)) @@ -50,7 +67,7 @@ (deftest canonical-tagged-value - (let [tval (reify data/TaggedValue + (let [tval (reify TaggedValue (edn-tag [this] 'foo) (edn-value [this] :bar/baz)) doc (canonize tval)]))