Commits

Steve Losh  committed 2a86296

Add paths, and clean up the tests with a nice macro or two.

  • Participants
  • Parent commits a178cb8

Comments (0)

Files changed (2)

File src/dram/parser.clj

            (tag-guts)))
 
 
+; Paths -----------------------------------------------------------------------
+(defparser path-char []
+  (choice (letter)
+          (digit)
+          (token #{\_ \-})))
+
+(defparser path-segment []
+  (let->> [contents (many1 (path-char))]
+    (always (apply str contents))))
+
+(defparser path []
+  (let->> [seg (path-segment)
+           segs (many (>> (char \.) (path-segment)))]
+    (always (concat [seg] segs))))
+
+
 ; Extends ---------------------------------------------------------------------
 (defparser tag-extends []
   (between (tag-open) (tag-close)

File test/dram/test/parser.clj

 (defmacro parses-as [input parser output]
   `(~'is (~'= ~output (run ~parser ~input))))
 
+(defmacro testing-parser [parser desc & data]
+  (let [pairs (partition 2 data)]
+    (apply list 'testing desc
+           (map (fn [[input output]]
+                  `(parses-as ~input ~parser ~output))
+                pairs))))
+
+(defmacro testing-parser-errors [parser desc & data]
+  (apply list 'testing desc
+         (map (fn [input]
+                `(is-error ~input ~parser))
+              data)))
+
 
 (deftest optional-test
-  (testing "Optional utility."
-    (parses-as "42" (p/optional (p/literal-integer)) 42)
-    (parses-as "a42" (p/optional (p/literal-integer)) nil)))
+  (testing-parser
+    (p/optional (p/literal-integer))
+    "The optional parser makes another parser optional!"
+
+    "42" 42
+    "a"  nil))
+
 (deftest whitespace-test
-  (testing "Optional whitespace parses to nil."
-    (parses-as "" (p/optional-whitespace) nil)
-    (parses-as "     " (p/optional-whitespace) nil)
-    (parses-as "\t" (p/optional-whitespace) nil)
-    (parses-as "\n" (p/optional-whitespace) nil)
-    (parses-as "\n \t " (p/optional-whitespace) nil))
-  (testing "Required whitespace parses to nil."
-    (parses-as "     " (p/required-whitespace) nil)
-    (parses-as "\t" (p/required-whitespace) nil)
-    (parses-as "\n" (p/required-whitespace) nil)
-    (parses-as "\n \t " (p/required-whitespace) nil))
-  (testing "Required whitespace is actually required."
-    (is-error "" (p/required-whitespace))
-    (is-error "foo " (p/required-whitespace))))
+  (testing-parser
+    (p/optional-whitespace) "Optional whitespace parses to nil."
+
+    ""       nil
+    "     "  nil
+    "\t"     nil
+    "\n"     nil
+    "\n \t " nil)
+
+  (testing-parser
+    (p/required-whitespace) "Required whitespace parses to nil."
+
+    "     "  nil
+    "\t"     nil
+    "\n"     nil
+    "\n \t " nil)
+
+  (testing-parser-errors
+    (p/required-whitespace) "Required whitespace is actually required."
+
+    ""
+    "foo"))
 
 (deftest integer-test
-  (testing "Parsing integers results in Clojure integers."
-    (parses-as "1" (p/literal-integer) 1)
-    (parses-as "10" (p/literal-integer) 10)
-    (parses-as "9234" (p/literal-integer) 9234))
-  (testing "Parsing negative integers."
-    (parses-as "-1" (p/literal-integer) -1)
-    (parses-as "-2945" (p/literal-integer) -2945))
-  (testing "Parsing garbage with the integer parser fails."
-    (is-error "" (p/literal-integer))
-    (is-error " 1" (p/literal-integer))
-    (is-error "foo" (p/literal-integer))
-    (is-error "-" (p/literal-integer))
-    (is-error " -1" (p/literal-integer))
-    (is-error "- 1" (p/literal-integer))
-    (is-error "-a12" (p/literal-integer))))
+  (testing-parser
+    (p/literal-integer) "Parsing integers results in Clojure integers."
+
+    "1"    1
+    "10"   10
+    "9234" 9234)
+
+  (testing-parser
+    (p/literal-integer) "Parsing negative integers results in integers too."
+
+    "-1"    -1
+    "-2945" -2945)
+
+  (testing-parser-errors
+    (p/literal-integer) "Parsing garbage with the integer parser fails."
+
+    ""
+    " 1"
+    "foo"
+    "-"
+    " -1"
+    "- 1"
+    "-a12"))
 
 (deftest string-test
-  (testing "Literal strings of simple characters parse to Clojure strings."
-    (parses-as "\"\"" (p/literal-string) "")
-    (parses-as "\"foo\"" (p/literal-string) "foo")
-    (parses-as "\" bar \"" (p/literal-string) " bar "))
-  (testing "Escape sequences are supported in strings."
-    (parses-as "\"a\\nb\"" (p/literal-string) "a\nb")
-    (parses-as "\"a\\\\b\"" (p/literal-string) "a\\b")
-    (parses-as "\"a\\\\nb\"" (p/literal-string) "a\\nb")
-    (parses-as "\"a\\\"b\"" (p/literal-string) "a\"b"))
-  (testing "Garbage doesn't parse as strings."
-    (is-error "foo" (p/literal-string))
-    (is-error "\"foo" (p/literal-string))
-    (is-error "foo\"" (p/literal-string)))
-  (testing "Parses the first bit as a string, so it should succeed (for now)."
-    (parses-as "\"fo\"o\"" (p/literal-string) "fo")))
+  (testing-parser
+    (p/literal-string)
+    "Literal strings of simple characters parse to Clojure strings."
+
+    "\"\""      ""
+    "\"foo\""   "foo"
+    "\" bar \"" " bar ")
+
+  (testing-parser
+    (p/literal-string)
+    "Escape sequences are supported in strings."
+
+    "\"a\\nb\""   "a\nb"
+    "\"a\\\\b\""  "a\\b"
+    "\"a\\\\nb\"" "a\\nb"
+    "\"a\\\"b\""  "a\"b")
+
+  (testing-parser-errors
+    (p/literal-string)
+    "Garbage doesn't parse as strings."
+
+    "foo"
+    "\"foo"
+    "foo\"")
+
+  (testing-parser
+    (p/literal-string)
+    "Parses the first bit as a string, so it should succeed (for now)."
+
+    "\"fo\"o\"" "fo"))
 
 (deftest literal-test
-  (testing "Literals can parse integers."
-    (parses-as "-42" (p/literal) -42)
-    (parses-as "585" (p/literal) 585))
-  (testing "Literals can parse strings."
-    (parses-as "\"foo\"" (p/literal-string) "foo")))
+  (testing-parser
+    (p/literal) "Literals can parse integers."
+
+    "-42" -42
+    "585" 585)
+
+  (testing-parser
+    (p/literal) "Literals can parse strings."
+
+    "\"foo\"" "foo"))
 
 (deftest variable-test
-  (testing "Variables can be simple literals (though I don't know why you'd bother)."
-    (parses-as "{{ 42 }}" (p/variable) 42)
-    (parses-as "{{ -2 }}" (p/variable) -2)
-    (parses-as "{{ \"foo\" }}" (p/variable) "foo"))
-  (testing "Variables can handle wonky whitespace."
-    (parses-as "{{42}}" (p/variable) 42)
-    (parses-as "{{ 42}}" (p/variable) 42)
-    (parses-as "{{42 }}" (p/variable) 42)
-    (parses-as "{{42       }}" (p/variable) 42)
-    (parses-as "{{\n\t\n\t42}}" (p/variable) 42)))
+  (testing-parser
+    (p/variable) "Variables can be simple literals."
+
+    "{{ 42 }}"      42
+    "{{ -2 }}"      -2
+    "{{ \"foo\" }}" "foo")
+
+  (testing-parser
+    (p/variable) "Variables can handle wonky whitespace."
+
+    "{{42}}"         42
+    "{{ 42}}"        42
+    "{{42 }}"        42
+    "{{42  }}"       42
+    "{{\n\t\n\t42}}" 42))
 
 (deftest extends-test
-  (testing "{% extends ... %} parses to its own custom AST element."
-    (parses-as "{% extends \"p\" %}" (p/tag-extends) {:type :extends :path "p"})
-    (parses-as "{% extends \"foo/bar\" %}" (p/tag-extends) {:type :extends
-                                                            :path "foo/bar"}))
-  (testing "{% extends ... %} requires a non-empty argument."
-    (is-error "{% extends \"\" %}" (p/tag-extends))
-    (is-error "{% extends %}" (p/tag-extends)))
-  (testing "{% extends ... %} doesn't accept garbage."
-    (is-error "{% extends foo %}" (p/tag-extends))
-    (is-error "{% extends \"foo\" foo %}" (p/tag-extends))
-    (is-error "{% extends foo \"foo\" %}" (p/tag-extends))
-    (is-error "{% extends foo\"foo\" %}" (p/tag-extends))
-    (is-error "{% extends 43 %}" (p/tag-extends))
-    (is-error "{% extends foo/bar %}" (p/tag-extends))
-    (is-error "{% extends the quick brown fox %}" (p/tag-extends))))
+  (testing-parser
+    (p/tag-extends)
+    "{% extends ... %} parses to its own custom AST element."
+
+    "{% extends \"p\" %}"        {:type :extends :path "p"}
+    "{% extends \"foo/bar\" %}"  {:type :extends :path "foo/bar"})
+
+  (testing-parser-errors
+    (p/tag-extends)
+    "{% extends ... %} requires a non-empty argument."
+
+    "{% extends \"\" %}"
+    "{% extends %}")
+
+  (testing-parser-errors
+    (p/tag-extends)
+    "{% extends ... %} doesn't accept garbage."
+
+    "{% extends foo %}"
+    "{% extends \"foo\" foo %}"
+    "{% extends foo \"foo\" %}"
+    "{% extends foo\"foo\" %}"
+    "{% extends 43 %}"
+    "{% extends foo/bar %}"
+    "{% extends the quick brown fox %}"))
 
 (deftest block-test
-  (testing "{% block ... %} parses to an intermediate AST element."
-    (parses-as "{% block cats %}" (p/tag-block-open) {:name "cats"})
-    (parses-as "{% block boots-and-cats %}" (p/tag-block-open)
-               {:name "boots-and-cats"})
-    (parses-as "{% block hello-world_585 %}" (p/tag-block-open)
-               {:name "hello-world_585"})
-    (parses-as "{% block a %}" (p/tag-block-open) {:name "a"})
-    (parses-as "{% block a_ %}" (p/tag-block-open) {:name "a_"}))
-  (testing "{% block ... %} requires valid block names."
-    (is-error "{% block 1 %}" (p/tag-block-open))
-    (is-error "{% block -1 %}" (p/tag-block-open))
-    (is-error "{% block -foo %}" (p/tag-block-open))
-    (is-error "{% block __foo %}" (p/tag-block-open))
-    (is-error "{% block 12dogs %}" (p/tag-block-open))
-    (is-error "{% block c&ats %}" (p/tag-block-open))
-    (is-error "{% block boots and cats %}" (p/tag-block-open))
-    (is-error "{% block \"rochester-made\" %}" (p/tag-block-open))
-    (is-error "{% block dogs* %}" (p/tag-block-open))
-    (is-error "{% block dogs% %}" (p/tag-block-open))
-    (is-error "{% block dogs} %}" (p/tag-block-open)))
-  (testing "{% block ... %} allows wonky whitespace."
-    (parses-as "{%block foo%}" (p/tag-block-open) {:name "foo"})
-    (parses-as "{%   block foo%}" (p/tag-block-open) {:name "foo"})
-    (parses-as "{%block      foo     %}" (p/tag-block-open) {:name "foo"})
-    (parses-as "{%\n\nblock\tfoo\n%}" (p/tag-block-open) {:name "foo"}))
-  (testing "{% block ... %} REQUIRES whitespace between block and the name."
-    (is-error "{% blockfoo %}" (p/tag-block-open)))
-  (testing "{% endblock %} parses to nil and allows weird whitespace."
-    (parses-as "{% endblock %}" (p/tag-block-close) nil)
-    (parses-as "{%\nendblock\t%}" (p/tag-block-close) nil)
-    (parses-as "{%endblock %}" (p/tag-block-close) nil)
-    (parses-as "{% endblock%}" (p/tag-block-close) nil))
-  (testing "{% endblock %} does NOT take a block name (for now)."
-    (is-error "{% endblock foo %}" (p/tag-block-open)))
-  (testing "Empty blocks are totally fine."
-    (parses-as "{% block foo %}{% endblock %}" (p/tag-block)
-               {:type :block :name "foo" :contents []}))
-  (testing "Blocks can contain anything except other blocks."
-    (parses-as "{% block foo %}hi{% endblock %}" (p/tag-block)
-               {:type :block :name "foo" :contents ["hi"]})
-    (parses-as "{% block foo %}hi {{ 1 }} five{% endblock %}" (p/tag-block)
-               {:type :block :name "foo" :contents ["hi " 1 " five"]})))
+  (testing-parser
+    (p/tag-block-open)
+    "{% block ... %} parses to an intermediate AST element."
+
+    "{% block cats %}"            {:name "cats"}
+    "{% block boots-and-cats %}"  {:name "boots-and-cats"}
+    "{% block hello-world_585 %}" {:name "hello-world_585"}
+    "{% block a %}"               {:name "a"}
+    "{% block a_ %}"              {:name "a_"})
+
+  (testing-parser-errors
+    (p/tag-block-open)
+    "{% block ... %} requires valid block names."
+
+    "{% block 1 %}"
+    "{% block -1 %}"
+    "{% block -foo %}"
+    "{% block __foo %}"
+    "{% block 12dogs %}"
+    "{% block c&ats %}"
+    "{% block boots and cats %}"
+    "{% block \"rochester-made\" %}"
+    "{% block dogs* %}"
+    "{% block dogs% %}"
+    "{% block dogs} %}")
+
+  (testing-parser
+    (p/tag-block-open)
+    "{% block ... %} allows wonky whitespace."
+
+    "{%block foo%}"           {:name "foo"}
+    "{%   block foo%}"        {:name "foo"}
+    "{%block      foo     %}" {:name "foo"}
+    "{%\n\nblock\tfoo\n%}"    {:name "foo"})
+
+  (testing-parser-errors
+    (p/tag-block-open)
+    "{% block ... %} REQUIRES whitespace between block and the name."
+
+    "{% blockfoo %}")
+
+  (testing-parser
+    (p/tag-block-close)
+    "{% endblock %} parses to nil and allows weird whitespace."
+
+    "{% endblock %}"   nil
+    "{%\nendblock\t%}" nil
+    "{%endblock %}"    nil
+    "{% endblock%}"    nil)
+
+  (testing-parser-errors
+    (p/tag-block-close)
+    "{% endblock %} does NOT take a block name (for now)."
+
+    "{% endblock foo %}")
+
+  (testing-parser
+    (p/tag-block)
+    "Empty blocks are totally fine."
+
+    "{% block foo %}{% endblock %}" {:type :block :name "foo" :contents []})
+
+  (testing-parser
+    (p/tag-block)
+    "Blocks can contain anything except other blocks."
+
+    "{% block foo %}hi{% endblock %}"
+    {:type :block :name "foo" :contents ["hi"]}
+
+    "{% block foo %}hi {{ 1 }} five{% endblock %}"
+    {:type :block :name "foo" :contents ["hi " 1 " five"]}))
 
 (deftest raw-text-test
-  (testing "Raw text parses to a Clojure string."
-    (parses-as "Hello" (p/raw-text) "Hello")
-    (parses-as "hello there world" (p/raw-text) "hello there world")
-    (parses-as "  { foo } is okay" (p/raw-text) "  { foo } is okay")
-    (parses-as "so is { % foo % }" (p/raw-text) "so is { % foo % }"))
-  (testing "Reserved characters do not parse as raw text."
-    (parses-as "Hello{{ world }}" (p/raw-text) "Hello")
-    (parses-as "Hello{% block world %}" (p/raw-text) "Hello"))
-  (testing "Raw text is not zero-length."
-    (is-error "{{ world }}" (p/raw-text))
-    (is-error "" (p/raw-text))))
+  (testing-parser
+    (p/raw-text) "Raw text parses to a Clojure string."
+
+    "Hello"             "Hello"
+    "hello there world" "hello there world"
+    "  { foo } is okay" "  { foo } is okay"
+    "so is { % foo % }" "so is { % foo % }")
+
+  (testing-parser
+    (p/raw-text) "Reserved characters do not parse as raw text."
+
+    "Hello{{ world }}"       "Hello"
+    "Hello{% block world %}" "Hello")
+
+  (testing-parser-errors
+    (p/raw-text) "Raw text is not zero-length."
+
+    "{{ world }}"
+    ""))
+
+(deftest path-test
+  (testing-parser
+    (p/path) "A path parses to a seq of strings."
+
+    "hello"             ["hello"]
+    "hello.world"       ["hello" "world"]
+    "users.0"           ["users" "0"]
+    "users.0.full-name" ["users" "0" "full-name"]
+    "0"                 ["0"]
+    "user.full_name"    ["user" "full_name"])
+
+  (testing-parser-errors
+    (p/path) "A path can't parse garbage."
+
+    "/foo"
+    "..oo"))
 
 (deftest template-chunk-test
- (testing "A template chunk can be raw text."
-    (parses-as "Hello" (p/template-chunk) "Hello")
-    (parses-as "  { foo }" (p/template-chunk) "  { foo }"))
-  (testing "A template chunk can be a variable."
-    (parses-as "{{ 1 }}" (p/template-chunk) 1)))
+  (testing-parser
+    (p/template-chunk) "A template chunk can be raw text."
+
+    "Hello"     "Hello"
+    "  { foo }" "  { foo }")
+
+  (testing-parser
+    (p/template-chunk) "A template chunk can be a variable."
+
+    "{{ 1 }}" 1))
 
 (deftest template-base-test
   (letfn [(bt [contents]
             {:type :base :contents contents})]
-    (testing "A base template can be made up of raw text, variables, ...."
-      (parses-as "" (p/template-base) (bt []))
-      (parses-as "Hello" (p/template-base)
-                 (bt ["Hello"]))
-      (parses-as "Hello {{ \"Steve\" }}" (p/template-base)
-                 (bt ["Hello " "Steve"]))
-      (parses-as "Age: {{ 27 }} years old" (p/template-base)
-                 (bt ["Age: " 27 " years old"])))
-    (testing "A base template can contain blocks."
-      (parses-as "{% block foo %}{% endblock %}" (p/template-base)
-                 (bt [{:type :block :name "foo" :contents []}]))
-      (parses-as "hello {% block username %}{% endblock %}" (p/template-base)
-                 (bt ["hello " {:type :block :name "username" :contents []}]))
-      (parses-as "foo {% block a %}{% endblock %} bar {{ 42 }}" (p/template-base)
-                 (bt ["foo "
-                      {:type :block :name "a" :contents []}
-                      " bar "
-                      42])))))
+    (testing-parser
+      (p/template-base)
+      "A base template can be made up of raw text, variables, ...."
+
+      ""                        (bt [])
+      "Hello"                   (bt ["Hello"])
+      "Hello {{ \"Steve\" }}"   (bt ["Hello " "Steve"])
+      "Age: {{ 27 }} years old" (bt ["Age: " 27 " years old"]))
+
+    (testing-parser
+      (p/template-base)
+      "A base template can contain blocks."
+
+      "{% block foo %}{% endblock %}"
+      (bt [{:type :block :name "foo" :contents []}])
+
+      "hello {% block username %}{% endblock %}"
+      (bt ["hello "
+           {:type :block :name "username" :contents []}])
+
+      "foo {% block a %}{% endblock %} bar {{ 42 }}"
+      (bt ["foo "
+           {:type :block :name "a" :contents []}
+           " bar "
+           42]))))
 
 (deftest template-child-test
   (letfn [(ct [extends blocks]
             {:type :child :extends extends :blocks blocks})]
-    (testing "A child template requires an extends tag."
-      (parses-as "{% extends \"a\" %}" (p/template-child)
-                 (ct "a" {}))
-      (parses-as "      {% extends \"a\" %}" (p/template-child)
-                 (ct "a" {}))
-      (parses-as "{% extends \"a\" %}\n\n" (p/template-child)
-                 (ct "a" {})))
-    (testing "A child template may contain blocks to override."
-      (parses-as "
-                 {% extends \"a\" %}
-                 {% block foo %}{% endblock %}
-                 "
-                 (p/template-child)
-                 (ct "a" {"foo" []}))
-      (parses-as "
-                 {% extends \"a\" %}
-                 {% block foo %}hello world{% endblock %}
-                 {% block bar %}{{ 10 }}{% endblock %}
-                 "
-                 (p/template-child)
-                 (ct "a" {"foo" ["hello world"]
-                          "bar" [10]})))))
+    (testing-parser
+      (p/template-child)
+      "A child template requires an extends tag."
+
+      "{% extends \"a\" %}"     (ct "a" {})
+      "  {% extends \"a\" %}"   (ct "a" {})
+      "{% extends \"a\" %}\n\n" (ct "a" {}))
+
+    (testing-parser
+      (p/template-child)
+      "A child template may contain blocks to override."
+
+      "
+      {% extends \"a\" %}
+      {% block foo %}{% endblock %}
+      "
+      (ct "a" {"foo" []})
+
+      "
+      {% extends \"a\" %}
+      {% block foo %}hello world{% endblock %}
+      {% block bar %}{{ 10 }}{% endblock %}
+      "
+      (ct "a" {"foo" ["hello world"]
+               "bar" [10]}))))