diff --git a/src/clojure_ini/core.clj b/src/clojure_ini/core.clj index 110cb83..6811ae5 100644 --- a/src/clojure_ini/core.clj +++ b/src/clojure_ini/core.clj @@ -1,15 +1,25 @@ (ns clojure-ini.core (:require [clojure.string :as s] + [clojure.walk :as w] [clojure.java.io :as io])) -(defn- parse-line [s kw trim] - (if (= (first s) \[) +(defn- parse-key + "Splits a key string by member-char and applies kw to each." + [kw member-char k] + (map kw (s/split k (re-pattern (str member-char))))) + +(defn- parse-line [s kw trim member-char expand-members?] + (if (= (first s) \[) (-> s (subs 1 (.indexOf s "]")) trim kw) (let [n (.indexOf s "=")] (if (neg? n) (throw (Exception. (str "Could not parse: " s))) - [(-> s (subs 0 n) trim kw) - (-> s (subs (inc n)) trim)])))) + (let [raw-key (-> s (subs 0 n) trim)] + (conj + (if expand-members? + (vec (parse-key kw member-char raw-key)) + [raw-key]) + (-> s (subs (inc n)) trim))))))) (defn- strip-comment [s chr allow-anywhere?] (let [n (.indexOf s (int chr))] @@ -25,16 +35,33 @@ (if (vector? x) (if (nil? key) (recur (rest xs) - (assoc m (first x) (second x)) + (assoc-in m (butlast x) (last x)) key) (recur (rest xs) - (assoc-in m [key (first x)] (second x)) + (assoc-in m (into [key] (butlast x)) (last x)) key)) (recur (rest xs) (assoc m x {}) x)) m))) +(defn- k->c [v] + (read-string + (if (keyword? v) + (subs (str v) 1) + (str v)))) + +(defn- listify + "Walks m and converts all maps with only numerically parsable keys into sequences." + [kw m] + (w/postwalk + #(if (map? %) + (if (every? (comp integer? k->c) (keys (dissoc % (kw "size")))) + (vals (into (sorted-map-by (fn [a b] (compare (k->c a) (k->c b)))) (dissoc % (kw "size")))) ;list + (into (sorted-map) %)) ;normal map + %) ;everything else + m)) + (defn read-ini "Read an .ini-file into a Clojure map. @@ -45,20 +72,31 @@ - trim? (default true): trim segments, keys and values - allow-comments-anywhere? (default true): Comments can appear anywhere, and not only at the beginning of a line - - comment-char (default \\;)" + - comment-char (default \\;) + - expand-members? (default false): expands keys seperated by member-char into a map hierarchy + - member-char (default \\/) + - listify? (default false): converts maps in the result with only numericaly parsable keys into sequences" [in & {:keys [keywordize? trim? allow-comments-anywhere? - comment-char] + comment-char + expand-members? + member-char + listify?] :or {keywordize? false trim? true allow-comments-anywhere? true - comment-char \;}}] + comment-char \; + expand-members? false + member-char \/ + listify? false}}] (let [kw (if keywordize? keyword identity) - trim (if trim? s/trim identity)] + trim (if trim? s/trim identity) + listify (if listify? listify #(identity %2))] (with-open [r (io/reader in)] (->> (line-seq r) (map #(strip-comment % comment-char allow-comments-anywhere?)) (remove (fn [s] (every? #(Character/isWhitespace %) s))) - (map #(parse-line % kw trim)) - mapify)))) + (map #(parse-line % kw trim member-char expand-members?)) + mapify + (listify kw))))) \ No newline at end of file diff --git a/test/clojure_ini/test/core.clj b/test/clojure_ini/test/core.clj index 2c1af79..31afafc 100644 --- a/test/clojure_ini/test/core.clj +++ b/test/clojure_ini/test/core.clj @@ -1,6 +1,161 @@ -(ns clj-ini.test.core - (:use [clj-ini.core]) - (:use [clojure.test])) +(ns clojure-ini.test.core + (:use [clojure-ini.core]) + (:use [clojure.test]) + (:use [clojure.pprint :only [pprint]]) + (:import java.io.StringWriter)) -(deftest replace-me ;; FIXME: write - (is false "No tests have been written.")) +(defn pprint-str [s] + (let [w (StringWriter.)] + (pprint s w) + (.toString w))) + +; TODO: Split into more specific smaller tests +(def test-string +"[base] +foo = 1 +bar = Baz + +[production] +database/host = 1.2.3.4 +database/user = root +database/password = abcdef +debug/enabled = false + +[development] +database/host = localhost +debug/enabled = true + +[testing] +database/host = 5.5.5.5 + +[addresses] +size = 12 +1 = 109.197.74.253 +2 = 223.242.215.25 +3 = 22.47.99.45 +4 = 21.250.15.30 +5 = 151.39.189.167 +6 = 35.187.190.135 +7 = 219.165.181.96 +8 = 128.35.227.236 +9 = 231.46.234.199 +10 = 33.195.224.55 +11 = 241.68.99.8 +12 = 63.114.37.214 + +[users] +size = 3 +0/name = \"Urist\" +0/pass = \"FooBar\" +1/name = \"John Doe\" +1/pass = \"password\" +2/name = \"MacFiddle\" +2/pass = \"123abc\"") + +(def test-expected-result-default + '{"addresses" + {"11" "241.68.99.8", + "size" "12", + "12" "63.114.37.214", + "1" "109.197.74.253", + "2" "223.242.215.25", + "3" "22.47.99.45", + "4" "21.250.15.30", + "5" "151.39.189.167", + "6" "35.187.190.135", + "7" "219.165.181.96", + "8" "128.35.227.236", + "9" "231.46.234.199", + "10" "33.195.224.55"}, + "users" + {"size" "3", + "2/name" "\"MacFiddle\"", + "1/name" "\"John Doe\"", + "0/name" "\"Urist\"", + "2/pass" "\"123abc\"", + "1/pass" "\"password\"", + "0/pass" "\"FooBar\""}, + "testing" {"database/host" "5.5.5.5"}, + "base" {"foo" "1", "bar" "Baz"}, + "production" + {"debug/enabled" "false", + "database/password" "abcdef", + "database/host" "1.2.3.4", + "database/user" "root"}, + "development" {"debug/enabled" "true", "database/host" "localhost"}}) + + +(def test-expected-result-keywords-expanded + '{:addresses + ("109.197.74.253" + "223.242.215.25" + "22.47.99.45" + "21.250.15.30" + "151.39.189.167" + "35.187.190.135" + "219.165.181.96" + "128.35.227.236" + "231.46.234.199" + "33.195.224.55" + "241.68.99.8" + "63.114.37.214"), + :base {:bar "Baz", :foo "1"}, + :development + {:database {:host "localhost"}, :debug {:enabled "true"}}, + :production + {:database {:host "1.2.3.4", :password "abcdef", :user "root"}, + :debug {:enabled "false"}}, + :testing {:database {:host "5.5.5.5"}}, + :users + ({:name "\"Urist\"", :pass "\"FooBar\""} + {:name "\"John Doe\"", :pass "\"password\""} + {:name "\"MacFiddle\"", :pass "\"123abc\""})}) + + +(def test-expected-result-strings-expanded + '{"addresses" + ("109.197.74.253" + "223.242.215.25" + "22.47.99.45" + "21.250.15.30" + "151.39.189.167" + "35.187.190.135" + "219.165.181.96" + "128.35.227.236" + "231.46.234.199" + "33.195.224.55" + "241.68.99.8" + "63.114.37.214"), + "base" {"bar" "Baz", "foo" "1"}, + "development" + {"database" {"host" "localhost"}, "debug" {"enabled" "true"}}, + "production" + {"database" {"host" "1.2.3.4", "password" "abcdef", "user" "root"}, + "debug" {"enabled" "false"}}, + "testing" {"database" {"host" "5.5.5.5"}}, + "users" + ({"name" "\"Urist\"", "pass" "\"FooBar\""} + {"name" "\"John Doe\"", "pass" "\"password\""} + {"name" "\"MacFiddle\"", "pass" "\"123abc\""})}) + + +(deftest test-data-default + (let [out (read-ini (.getBytes test-string))] + (is (= out test-expected-result-default) + (pprint-str out)))) + +(deftest test-data-keywords-expanded + (let [out (read-ini (.getBytes test-string) + :keywordize? true + :listify? true + :expand-members? true)] + (is (= out test-expected-result-keywords-expanded) + (pprint-str out)))) + +(deftest test-data-strings-expanded + (let [out (read-ini (.getBytes test-string) + :keywordize? false + :listify? true + :expand-members? true)] + (is (= out test-expected-result-strings-expanded) + (pprint-str out)))) \ No newline at end of file