diff --git a/deps.edn b/deps.edn index f36c04a..8a9e05d 100644 --- a/deps.edn +++ b/deps.edn @@ -2,4 +2,9 @@ :deps {org.clojure/clojure {:mvn/version "1.11.3"}} :aliases {:test {:extra-paths ["test"] :extra-deps {lambdaisland/kaocha {:mvn/version "1.88.1376"}} - :main-opts ["-m" "kaocha.runner"]}}} + :main-opts ["-m" "kaocha.runner"]} + :doc-gen {:extra-deps {marginalia/marginalia {:mvn/version "0.9.2"}} + :main-opts ["-m" "marginalia.main" + "-n" "Aurelio" + "-f" "index.html" + "src"]}}} diff --git a/docs/01-why.org b/docs/01-why.org index e246905..0e4dbb7 100644 --- a/docs/01-why.org +++ b/docs/01-why.org @@ -52,39 +52,44 @@ mock ::= endpoint* And this is what it looks in Aurelio syntax: #+begin_src clojure - {;; yaml - :indent [:+ [:| ["\t" " "]]] - :seqb [:| [#block "-" #flow "["]] - :seqe [:| [#block :empty #flow "]"]] - :mapb [:| [#block :empty #flow "{"]] - :mape [:| [#block :empty #flow "}"]] - :multiline #block [:| "|" ">"] - - ;; mock - :method [:| ["GET" "HEAD" "POST" "PUT" "DELETE" - "CONNECT" "OPTIONS" "TRACE"]] - :path [:+ "/" :string] - :header [:string ":" :string] - :response [:mapb [:ul - [:? "status" ":" [:... [200 400]]] - [:? "headers" ":" :mapb [:+ header] :mape] - ["body" ":" [:? :multiline] :string]] - :mape] - :webhook [:mapb [:ul - [:? "sleep-time" ":" :int] - [:? "if" ":" :string] - ["url" ":" :string] - ["method" ":" :method] - ["body" ":" [:? :multiline] :string]] - :mape] - :endpoint [:seqb "endpoint" - :mapb [:ul - [:? "method" ":" :method] - ["path" ":" :path] - ["response" ":" :response] - ["webhook" ":" :webhook]] - :mape :seqe] - :mock [:* :endpoint]} + {;; yaml + :indent [:seq+ [:or \tab \space]] + :seqb [:or \- \[] + :seqe [:or :empty \]] + :mapb [:or :empty \{] + :mape [:or :empty \}] + :quote [:opt [:or \' \"]] + :string [:quote :str :quote] + :multiline [:or \| \>] + + ;; mock + :method [:or "GET" "HEAD" "POST" "PUT" "DELETE" "CONNECT" "OPTIONS" "TRACE"] + :path [:seq+ \/ :string] + :header [:string \: :string] + :response [:mapb + [:useq + [:opt "status" \: [:range ["200" "400"]]] + [:opt "headers" \: :mapb [:seq+ :header] :mape] + "body" ":" [:opt :multiline] :string] + :mape] + :webhook [:mapb + [:useq + [:opt "sleep-time" \: :int] + [:opt "if" \: :string] + ["url" \: :string] + ["method" \: :method] + ["body" \: [:opt :multiline] :string]] + :mape] + :endpoint [:seqb "endpoint" + :mapb + [:useq + [:? "method" \: :method] + ["path" \: :path] + ["response" \: :response] + ["webhook" \: :webhook]] + :mape + :seqe] + :mock [:seq* :endpoint]} #+end_src The last one will be the one considered when parsing a given arg. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..0aebdde --- /dev/null +++ b/docs/index.html @@ -0,0 +1,3084 @@ + +Aurelio -- Marginalia

Aurelio


+



(this space intentionally left almost blank)
 
+
(ns aurelio.core)
+
(defn -main
+  [& _args]
+  (prn "hello world"))
 
+
(ns aurelio.grammar)

Returns a predicate function that compares given character +c1 against any other character.

+
(defn build-char-p
+  [^Character c1]
+  (fn [^Character c2]
+    (= (int c1) (int c2))))

Returns a predicate function that compares given string +s1 agains any other string.

+
(defn build-str-p
+  [^String s1]
+  (fn [^String s2]
+    (= s1 s2)))

Returns a predicate function that tests given list of +predicates against any expression.

+
(defn build-or-p
+  ([]
+   (throw (Exception. "`or` predicate has no inner predicates.")))
+  ([& predicates]
+   (fn [expr]
+     ((complement not-any?) #(% expr) predicates))))

Returns a predicate function that tests whether any other +character exists inbetween start and end.

+ +

NOTE: Currently, Aurelio does not support UTF-8 or any other +encoding format, only ASCII. Please open a PR incase you need +that working ASAP.

+
(defn build-range-p
+  [start end]
+  (fn [c]
+    (<= (int start) (int c) (int end))))

Returns a predicate function that tests given predicate against +a sequence of expressions that MAY be null or empty.

+
(defn build-seq*-p
+  [predicate]
+  (fn [exprs]
+    (every? #(predicate %) (filter some? (seq exprs)))))

Returns a predicate function that tests given predicate against +a sequence of expressions that CANNOT be nil or empty.

+
(defn build-seq+-p
+  [predicate]
+  (fn [exprs]
+    (and (some? (seq exprs)) ((build-seq*-p predicate) exprs))))

Returns a predicate function that tests given predicate against +any expression, as long as it is not nil.

+
(defn build-opt-p
+  [predicate]
+  (fn [& expr]
+    (or (empty? expr) (predicate (first expr)))))
+
(defn build-useq-p
+  ([]
+   (throw (Exception. "`useq` predicate has no predicates.")))
+  ([& predicates]
+   (fn [exprs]
+     ;; compare non-optionals with predicate count
+     (if (not= (count (remove #(when (seq? %)
+                                 (not= (first %) :opt))
+                              exprs))
+               (count predicates))
+       (throw (Exception. "expression count != predicate count"))
+       (let [preds-or-p (apply build-or-p predicates)]
+         (map #(preds-or-p %) exprs))))))

((build-useq-p (build-char-p \c) (build-char-p \t)) [\t \t])

+
+
(def predicate-builders
+  {:or build-or-p
+   :range build-range-p
+   :seq* build-seq*-p
+   :seq+ build-seq+-p
+   :opt build-seq*-p
+   :useq build-useq-p})

Given a list of expressions (the vals for a grammar table), +separates the symbol references and reordes them by reference count.

+ +

A symbol reference is a keyword that is not a builtin predicate, a +a reference to a symbol created by the user.

+ +

NOTE: This algorithm should be refactored in the future. We're currently +ignoring circular dependencies. Also, it looks complex :(

+
(defn- sort-sym-refs
+  [exprs]
+  (reduce
+   (fn [acc expr]
+     (->> (flatten expr)
+          ;; retrieve symbol references
+          (filter #(and (keyword? %) (nil? (get predicate-builders %))))
+          ;; count symrefs by parent
+          (reduce #(update-in %1 [%2] (fnil inc 0)) {})
+          ;; count all unique symrefs
+          (reduce-kv
+           (fn [sym-acc wsym wsym-count]
+             (update-in sym-acc [wsym] (fnil (partial + wsym-count) 0)))
+           acc)
+          (sort-by val >)
+          (into {})))
+   {} exprs))
+
(comment
+  (sort-sym-refs [[:a :b [:or :d :d]]
+                  [:b [:seq+ :d :d]]])
+  ;; => {:d 4, :b 2, :a 1}
+  )

Given a grammar, reorders its symbols (i.e. keywords) based +on their reference count by other symbol's expressions.

+ +

See also: sort-sym-refs

+
(defn order-syms-by-dep
+  [grammar]
+  (let [sym-refs (sort-sym-refs (vals grammar))]
+    (->> grammar
+         (sort-by #(get sym-refs (key %) 0) >)
+         (into {}))))

Builds a list of predicates, corresponding to each expression in given expressions.

+
(defn build-expr-p
+  [expr]
+  (if (empty? expr)
+    (throw (Exception. "an empty expression is invalid."))
+    (let [pred-keyword (first expr)
+          pred-builder (get predicate-builders pred-keyword)]
+      (if (and (keyword? pred-keyword) (some? pred-builder))
+        (try
+          (->> (build-expr-p (rest expr))
+               (apply pred-builder))
+          (catch Exception e
+            (throw (Exception. (str "failed to build predicate `"
+                                    pred-keyword "`\n"
+                                    (.getMessage e))))))
+        (map
+         (fn [inner-expr]
+           (cond-> inner-expr
+             (char? inner-expr) build-char-p
+             (string? inner-expr) build-str-p
+             (keyword? inner-expr) identity
+             (vector? inner-expr) build-expr-p))
+         expr)))))
+
(def base-grammar
+  {:ws [\space]
+   :nl [\n]
+   :empty []})
+
(defn build
+  [user-grammar]
+  (let [grammar (order-syms-by-dep (merge base-grammar user-grammar))]
+    (reduce-kv
+     (fn [gr sym expr]
+       (assoc gr sym (build-expr-p expr)))
+     {} grammar)))
 
\ No newline at end of file diff --git a/src/aurelio/grammar.clj b/src/aurelio/grammar.clj index f3a2920..d423395 100644 --- a/src/aurelio/grammar.clj +++ b/src/aurelio/grammar.clj @@ -1,21 +1,27 @@ (ns aurelio.grammar) (defn build-char-p - "Returns a predicate function that can compare given character + "Returns a predicate function that compares given character `c1` against any other character." [^Character c1] (fn [^Character c2] (= (int c1) (int c2)))) +(defn build-str-p + "Returns a predicate function that compares given string + `s1` agains any other string." + [^String s1] + (fn [^String s2] + (= s1 s2))) + (defn build-or-p - "Returns a predicate function that runs given list of - `predicates` against any other value, returning true if - any of them returns true as well." + "Returns a predicate function that tests given list of + `predicates` against any expression." ([] (throw (Exception. "`or` predicate has no inner predicates."))) ([& predicates] - (fn [v] - ((complement not-any?) #(% v) predicates)))) + (fn [expr] + ((complement not-any?) #(% expr) predicates)))) (defn build-range-p "Returns a predicate function that tests whether any other @@ -29,16 +35,52 @@ (<= (int start) (int c) (int end)))) (defn build-seq*-p + "Returns a predicate function that tests given `predicate` against + a sequence of expressions that `MAY` be null or empty." + [predicate] + (fn [exprs] + (every? #(predicate %) (filter some? (seq exprs))))) + +(defn build-seq+-p + "Returns a predicate function that tests given `predicate` against + a sequence of expressions that `CANNOT` be `nil` or empty." + [predicate] + (fn [exprs] + (and (some? (seq exprs)) ((build-seq*-p predicate) exprs)))) + +(defn build-opt-p + "Returns a predicate function that tests given `predicate` against + any expression, as long as it is not `nil`." [predicate] - (fn [vals] - (every? #(predicate %) vals))) + (fn [& expr] + (or (empty? expr) (predicate (first expr))))) + +;; TODO: improve this, too weak +(defn build-useq-p + "Returns a predicate function that tests if all non-optional expressions + match uniquely (independent from order) each given `predicate`." + ([] + (throw (Exception. "`useq` predicate has no predicates."))) + ([& predicates] + (fn [exprs] + ;; compare non-optionals with predicate count + (if (not= (count (remove #(when (seq? %) + (not= (first %) :opt)) + exprs)) + (count predicates)) + (throw (Exception. "expression count != predicate count")) + (let [preds-or-p (apply build-or-p predicates)] + (map #(preds-or-p %) exprs)))))) + +;; ((build-useq-p (build-char-p \c) (build-char-p \t)) [\t \t]) (def predicate-builders {:or build-or-p :range build-range-p :seq* build-seq*-p - :seq+ build-seq*-p - :opt build-seq*-p}) + :seq+ build-seq+-p + :opt build-seq*-p + :useq build-useq-p}) (defn- sort-sym-refs "Given a list of `expressions` (the vals for a grammar table), @@ -48,7 +90,8 @@ a reference to a symbol created by the user. `NOTE`: This algorithm should be refactored in the future. We're currently - ignoring circular dependencies. Also, it looks complex :(" + ignoring circular dependencies. Also, it looks complex :( + " [exprs] (reduce (fn [acc expr] @@ -92,16 +135,17 @@ pred-builder (get predicate-builders pred-keyword)] (if (and (keyword? pred-keyword) (some? pred-builder)) (try - (->> (rest expr) - build-expr-p + (->> (build-expr-p (rest expr)) (apply pred-builder)) (catch Exception e - (throw (Exception. (str "failed to build predicate `" pred-keyword "`\n" + (throw (Exception. (str "failed to build predicate `" + pred-keyword "`\n" (.getMessage e)))))) (map (fn [inner-expr] (cond-> inner-expr (char? inner-expr) build-char-p + (string? inner-expr) build-str-p (keyword? inner-expr) identity (vector? inner-expr) build-expr-p)) expr))))) @@ -109,13 +153,7 @@ (def base-grammar {:ws [\space] :nl [\n] - :char [:or - [:range \a \z] - [:range \A \Z]] - :string [:seq+ :char] - :digit [:range \0 \9] - :number [[:seq+ :digit] - [:opt [\. [:seq+ :digit]]]]}) + :empty [""]}) (defn build [user-grammar] diff --git a/test/aurelio/grammar_test.clj b/test/aurelio/grammar_test.clj index 489fee9..0c53abb 100644 --- a/test/aurelio/grammar_test.clj +++ b/test/aurelio/grammar_test.clj @@ -7,6 +7,10 @@ [(t/is ((g/build-char-p \c) \c)) (t/is (not ((g/build-char-p \c) \t)))]) +(t/deftest build-str-p-test + [(t/is ((g/build-str-p "hello") "hello")) + (t/is (not ((g/build-str-p "hello") "bye")))]) + (t/deftest build-or-p-test (let [or-ct-p (g/build-or-p (g/build-char-p \c) @@ -48,7 +52,7 @@ :a [:or [:seq+ :d] :e] :b [\c [:seq* :e] :d :d]})) -(t/deftest build-expr-p +(t/deftest build-expr-p-test (t/testing "char expr" (let [[space-p c-p] (g/build-expr-p [\space \c])] [(t/is (space-p \space)) @@ -65,6 +69,27 @@ (t/is (n-p \n)) (t/is (not (n-p \a)))]))) +(t/deftest build-seq-p-test + (let [alphachar-p (g/build-or-p + (g/build-range-p \a \z) + (g/build-range-p \A \Z))] + (t/testing "seq*" + (let [alphastr*-p (g/build-seq*-p alphachar-p)] + [(t/is (alphastr*-p [])) + (t/is (alphastr*-p nil)) + (t/is (alphastr*-p [\h \e \l \l \o])) + (t/is (not (alphastr*-p [\h \3 \l \l \o])))])) + (t/testing "seq+" + (let [alphastr+-p (g/build-seq+-p alphachar-p)] + [(t/is (alphastr+-p [\h \e \l \l \o])) + (t/is (not (alphastr+-p [\3])))])))) + +(t/deftest build-opt-p-test + (let [c-p (g/build-opt-p (g/build-char-p \c))] + [(t/is (c-p)) + (t/is (c-p \c)) + (t/is (not (c-p \t)))])) + ;; TODO #_(t/deftest build (t/testing "empty user grammar"