From d7a3c4de46053c3bfe3d599640ba5fe38f03f954 Mon Sep 17 00:00:00 2001 From: Respatialized Date: Wed, 11 Dec 2024 18:49:26 -0500 Subject: [PATCH] Add API test to ensure all Fabricate public vars are documented --- deps.edn | 95 ++++++++----------- src/site/fabricate/api.clj | 3 + src/site/fabricate/prototype/hiccup.clj | 3 + src/site/fabricate/prototype/html.clj | 38 +++++++- src/site/fabricate/prototype/read.clj | 13 ++- src/site/fabricate/prototype/read/grammar.clj | 2 +- src/site/fabricate/prototype/schema.clj | 15 +-- test/site/fabricate/api_test.clj | 33 +++++++ test/site/fabricate/prototype/check.clj | 33 +++---- .../prototype/source/clojure_test.clj | 2 +- 10 files changed, 142 insertions(+), 95 deletions(-) diff --git a/deps.edn b/deps.edn index 3466137..ac3aa2b 100644 --- a/deps.edn +++ b/deps.edn @@ -1,57 +1,38 @@ -{:aliases - {:dev {:extra-deps {babashka/babashka.curl {:mvn/version "0.1.2"} - com.clojure-goes-fast/clj-async-profiler - {:mvn/version "1.1.1"} - com.clojure-goes-fast/clj-memory-meter {:mvn/version - "0.3.0"} - com.cnuernber/charred {:mvn/version "1.033"} - com.kiranshila/cybermonday {:mvn/version "0.6.215"} - com.taoensso/tufte {:mvn/version "2.6.3"} - com.vladsch.flexmark/flexmark-ext-wikilink - {:mvn/version "0.64.8"} - criterium/criterium {:mvn/version "0.4.6"} - dev.onionpancakes/chassis {:mvn/version "1.0.365"} - garden/garden {:mvn/version "1.3.10"} - juxt/dirwatch {:mvn/version "0.2.5"} - zprint/zprint {:mvn/version "1.2.9"}} - :extra-paths ["test" "dev"] - :jvm-opts ["-Djdk.attach.allowAttachSelf" - "-XX:+UnlockDiagnosticVMOptions" - "-XX:+DebugNonSafepoints" - "-Dsite.fabricate.mode=dev"]} - :runner {:exec-args {:excludes [:performance] - :patterns ["^(?!.*performance).*$"]} - :exec-fn cognitect.test-runner.api/test - :extra-deps {io.github.cognitect-labs/test-runner - {:git/url - "https://github.com/cognitect-labs/test-runner" - :sha "7284cda41fb9edc0f3bc6b6185cfb7138fc8a023"}} - :main-opts ["-m" "cognitect.test-runner"]} - :serve {:main-opts ["-m" "http.server" "--dir" "docs"]} - :test {:extra-deps {babashka/babashka.curl {:mvn/version "0.1.2"} - com.brunobonacci/mulog {:mvn/version "0.9.0"} - com.gfredericks/test.chuck {:mvn/version "0.2.14"} - nu.validator/validator {:mvn/version "20.7.2"} - org.clojure/test.check {:mvn/version "1.1.1"} - site.fabricate/manual - {:git/url - "https://github.com/fabricate-site/manual.git" - :sha "ae660ad598f0c3d01beb6250789db8cc5a54a69a"}} - :extra-paths ["test"]} - :validate {:exec-args {:dirs ["docs/"]} - :exec-fn site.fabricate.prototype.check/html}} - :deps - {babashka/fs {:mvn/version "0.2.12"} - hiccup/hiccup {:mvn/version "2.0.0-RC2"} - instaparse/instaparse {:mvn/version "1.4.12"} - metosin/malli {:mvn/version "0.13.0"} - nasus/nasus {:git/url "https://github.com/kachayev/nasus/" - :sha "8923283db09f88f3503d0246bad31eea15341138"} - org.clojure/clojure {:mvn/version "1.11.3"} - org.clojure/data.finger-tree {:mvn/version "0.0.3"} - org.scicloj/kindly {:mvn/version "4-alpha16"} - rewrite-clj/rewrite-clj {:mvn/version "1.1.47"} - site.fabricate/adorn {:git/tag "v0.1.131-alpha" - :git/url "https://github.com/fabricate-site/adorn" - :sha "6a9a87959537a71760cd08f8fc2a292ba4957755"}} - :paths ["src"]} +{:aliases {:runner {:exec-args {:excludes [:performance] + :patterns ["^(?!.*performance).*$"]} + :exec-fn cognitect.test-runner.api/test + :extra-deps + {io.github.cognitect-labs/test-runner + {:git/url "https://github.com/cognitect-labs/test-runner" + :sha "7284cda41fb9edc0f3bc6b6185cfb7138fc8a023"}} + :main-opts ["-m" "cognitect.test-runner"]} + :test {:extra-deps + {babashka/babashka.curl {:mvn/version "0.1.2"} + com.brunobonacci/mulog {:mvn/version "0.9.0"} + com.gfredericks/test.chuck {:mvn/version "0.2.14"} + nu.validator/validator {:mvn/version "20.7.2"} + org.clojure/test.check {:mvn/version "1.1.1"} + site.fabricate/manual + {:git/url "https://github.com/fabricate-site/manual.git" + :sha "ae660ad598f0c3d01beb6250789db8cc5a54a69a"}} + :extra-paths ["test"]} + :validate {:exec-args {:dirs ["docs/"]} + :exec-fn site.fabricate.prototype.check/html}} + :deps {babashka/fs {:mvn/version "0.2.12"} + hiccup/hiccup {:mvn/version "2.0.0-RC2"} + instaparse/instaparse {:mvn/version "1.4.12"} + metosin/malli {:mvn/version "0.13.0"} + nasus/nasus {:git/url + "https://github.com/kachayev/nasus/" + :sha + "8923283db09f88f3503d0246bad31eea15341138"} + org.clojure/clojure {:mvn/version "1.11.3"} + org.clojure/data.finger-tree {:mvn/version "0.0.3"} + org.scicloj/kindly {:mvn/version "4-alpha16"} + rewrite-clj/rewrite-clj {:mvn/version "1.1.47"} + site.fabricate/adorn {:git/tag "v0.1.131-alpha" + :git/url + "https://github.com/fabricate-site/adorn" + :sha + "6a9a87959537a71760cd08f8fc2a292ba4957755"}} + :paths ["src"]} diff --git a/src/site/fabricate/api.clj b/src/site/fabricate/api.clj index 8f1626a..abf7265 100644 --- a/src/site/fabricate/api.clj +++ b/src/site/fabricate/api.clj @@ -140,6 +140,9 @@ collect-dispatch) (def site-schema + "Malli schema describing the contents of a Fabricate site. + +A site is the primary map passed between the 3 core API functions: plan!, assemble, and construct!" (m/schema [:map [:site.fabricate.api/entries [:* entry-schema]] [:site.fabricate.api/options :map]])) diff --git a/src/site/fabricate/prototype/hiccup.clj b/src/site/fabricate/prototype/hiccup.clj index aef7d13..51b4107 100644 --- a/src/site/fabricate/prototype/hiccup.clj +++ b/src/site/fabricate/prototype/hiccup.clj @@ -133,6 +133,7 @@ ([form] (parse-paragraphs form {}))) (defn ->meta + "Convert the given key/value pair to a Hiccup/HTML metadata element." {:malli/schema [:=> [:cat [:schema [:cat :any :any]]] [:cat [:= :meta] :map]]} [[k v]] (let [attrs (if (map? v) v {:content v})] @@ -140,6 +141,7 @@ (def default-metadata-map + "Default metadata for Fabricate HTML pages." {:title "Fabricate" :description "Fabricate: static website generation for Clojure" "viewport" "width=device-width, initial-scale=1.0" @@ -148,6 +150,7 @@ :site-title "Fabricate"}) (def default-metadata + "Additional default metadata for Fabricate HTML pages." (list [:meta {:charset "utf-8"}] [:meta {:http-equiv "X-UA-Compatible" :content "IE=edge"}])) diff --git a/src/site/fabricate/prototype/html.clj b/src/site/fabricate/prototype/html.clj index 124411b..f51d70a 100644 --- a/src/site/fabricate/prototype/html.clj +++ b/src/site/fabricate/prototype/html.clj @@ -76,11 +76,15 @@ #{:ins :del :object}) (def external-link-pattern + "Regex pattern for external URLs." (re-pattern "https?://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")) (def internal-link-pattern + "Regex pattern for internal URLs." (re-pattern "/[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")) -(def url [:or [:re external-link-pattern] [:re internal-link-pattern]]) +(def url + "Malli schema for URLs." + [:or [:re external-link-pattern] [:re internal-link-pattern]]) (def global-attributes "MDN list of global HTML attributes as malli schema" @@ -91,9 +95,12 @@ [:itemscope :boolean] [:itemtype [:re external-link-pattern]] [:lang [:enum "en"]] [:tabindex :int] [:title :string] [:part :string]])) -(def atomic-element [:or :boolean :double :int :string :nil]) +(def atomic-element + "Malli schema representing atomic HTML/Hiccup elements" + (m/schema [:or :boolean :double :int :string :nil])) (def ^{:malli/schema [:=> [:cat :any] :boolean]} atomic-element? + "Returns true if the given value is an atomic HTML/Hiccup element" (m/validator atomic-element)) (defn- ->hiccup-schema @@ -110,6 +117,7 @@ (->hiccup-schema tag attr-schema content-schema {}))) (defn ns-kw + "Convert the given keyword to a namespaced version, using the current ns if not provided." {:malli/schema [:=> [:cat [:? [:fn #(.isInstance clojure.lang.Namespace %)]] :keyword] :keyword]} @@ -612,18 +620,27 @@ [:schema [:ref ::heading-content]] [:schema [:ref ::phrasing-content]]]}} ::html])) -(def element (m/schema (schema/subschema html ::element))) +;; TODO: figure out how to register all of these schemas/subschemas +;; in the global registry specified by the site.fabricate.prototype.schema ns +(def element + "Malli schema for HTML elements." + (m/schema (schema/subschema html ::element))) -(def ^{:malli/schema [:=> [:cat :any] :boolean]} element? (m/validator element)) +(def ^{:malli/schema [:=> [:cat :any] :boolean]} element? + "Returns true if the given value is a HTML element." + (m/validator element)) (def ^{:malli/schema [:=> [:cat :any] [:or :map [:= :malli.core/invalid]]]} parse-element + "Parse the given value as a HTML element." (m/parser element)) (def ^{:malli/schema [:=> [:cat :any] [:or :map :nil]]} explain-element + "Explain why the given value is not a HTML element." (m/explainer element)) (def element-flat + "Simplified schema for HTML elements that does not distinguish between flow, phrasing, heading, or metadata content." (let [elems (->> (dissoc (get (m/properties html) :registry) ::flow-content ::heading-content @@ -642,48 +659,60 @@ (seq elems))))) (def ^{:malli/schema [:=> [:cat :any] [:or :map :nil]]} element-flat-explainer + "Explain why the given value does not match the schema for flat HTML content." (m/explainer element-flat)) (def ^{:malli/schema [:=> [:cat :any] [:or :map [:= :malli.core/invalid]]]} parse-element-flat + "Parse the given value according to the schema for flat HTML content." (m/parser element-flat)) (def element-validators + "Map with tags (keys) and predicates (values) that check if a value is a HTML element of the given type." (let [kws (filter keyword? (keys (get (m/properties html) :registry)))] (into {:atomic-element (m/validator atomic-element)} (map (fn [t] [t (m/validator (schema/subschema html (ns-kw t)))]) kws)))) (def element-explainers + "Map with tags (keys) and functions (values) that explain why a value does not match the schema for the HTML element of the given type." (let [kws (filter keyword? (keys (get (m/properties html) :registry)))] (into {:atomic-element (m/explainer atomic-element)} (map (fn [t] [t (m/explainer (schema/subschema html (ns-kw t)))]) kws)))) (def element-parsers + "Map with tags (keys) and functions (values) that parse a HTML element of the specific type." (let [kws (filter keyword? (keys (get (m/properties html) :registry)))] (into {:atomic-element (m/parser atomic-element)} (map (fn [t] [t (m/parser (schema/subschema html (ns-kw t)))]) kws)))) +;; TODO: make this function actually work + ;; "content is palpable when it's neither empty or hidden; ;; it is content that is rendered and is substantive. ;; Elements whose model is flow content or phrasing content ;; should have at least one node which is palpable." (defn palpable? + "Returns true if the node is visible." {:malli/schema [:=> [:cat :any] :boolean]} [c] (some? (some #(m/validate atomic-element %) (tree-seq #(and (vector? %) (keyword? (first %))) rest c)))) (def ^{:malli/schema [:=> [:cat :any] :boolean]} phrasing? + "Returns true if the element is HTML phrasing content." (m/validator (schema/subschema html ::phrasing-content))) (def ^{:malli/schema [:=> [:cat :any] :boolean]} heading? + "Returns true if the element is HTML heading content." (m/validator (schema/subschema html ::heading-content))) (def ^{:malli/schema [:=> [:cat :any] :boolean]} flow? + "Returns true if the element is HTML flow content." (m/validator (schema/subschema html ::flow-content))) (defn validate-element + "Attempt to validate the element based on its tag, or the keyword in the first position of the vector." {:malli/schema [:=> [:cat :any] :boolean]} [elem] (if (and (vector? elem) (keyword? (first elem))) @@ -709,6 +738,7 @@ (when schema (last (flatten (last (m/form schema)))))))) (def tag-contents + "Map with permitted contents across all tags." (->> (concat phrasing-tags flow-tags heading-tags sectioning-tags metadata-tags) (map (fn [t] [t (permitted-contents t)])) diff --git a/src/site/fabricate/prototype/read.clj b/src/site/fabricate/prototype/read.clj index d41b0a2..80c9bb3 100644 --- a/src/site/fabricate/prototype/read.clj +++ b/src/site/fabricate/prototype/read.clj @@ -36,6 +36,7 @@ [:file {:optional true :doc "The source file of the expression"} :string]])) (def ^{:malli/schema [:=> [:cat :any] :boolean]} fabricate-expr? + "Returns true if the given value matches the schema for parsed Fabricate expressions." (m/validator parsed-expr-schema)) (def evaluated-expr-schema @@ -84,13 +85,13 @@ '() (map (fn [e] {:expr-src (str e) :expr e}) (read-str (str open front-matter close))))] - (with-meta - (cond (= delims "[]") (apply conj [] (concat parsed-front-matter forms)) - (= delims "()") (concat () parsed-front-matter forms)) - (meta ext-form)))) + (with-meta (cond (= delims "[]") (into [] + (concat parsed-front-matter forms)) + (= delims "()") (concat () parsed-front-matter forms)) + (meta ext-form)))) (def parsed-schema - "Malli schema describing the elements of a fabricate template after it has been parsed by the Instaparse grammar" + "Malli schema describing the elements of a fabricate template after it has been parsed by the Instaparse grammar." (m/schema [:schema {:registry {::txt [:tuple {:encode/get {:leave second}} [:= :txt] :string] @@ -156,12 +157,14 @@ ;; TODO: this probably needs to be made more robust (defn read-error? + "Returns true if the given expression failed to read into a valid Clojure form." {:malli/schema [:=> [:cat parsed-expr-schema] :boolean]} [error-form] (= "Unexpected EOF." (get-in error-form [:error :data :msg]))) ;; TODO: should this be a multimethod? (defn error->hiccup + "Return a Hiccup form with context for the error." {:malli/schema [:=> [:cat parsed-expr-schema] error-form-schema]} [{:keys [expr-src exec expr error result display] :as parsed-expr}] [:div {:class "fabricate-error"} [:h6 "Error"] diff --git a/src/site/fabricate/prototype/read/grammar.clj b/src/site/fabricate/prototype/read/grammar.clj index 862dd50..332fadb 100644 --- a/src/site/fabricate/prototype/read/grammar.clj +++ b/src/site/fabricate/prototype/read/grammar.clj @@ -36,7 +36,7 @@ terminal-lookahead "(?=\\Z|(?:[\\]})]//🔚|✳|🔚))"] (str fast-possessive "|(" reluctant-txt terminal-lookahead ")"))) -(def delimiters ["✳" "🔚"]) +(def delimiters "Default delimiters for Fabricate." ["✳" "🔚"]) (def grammar "The formal grammar for Fabricate templates." diff --git a/src/site/fabricate/prototype/schema.clj b/src/site/fabricate/prototype/schema.clj index 7a0d8b9..9c08746 100644 --- a/src/site/fabricate/prototype/schema.clj +++ b/src/site/fabricate/prototype/schema.clj @@ -1,5 +1,5 @@ (ns site.fabricate.prototype.schema - "Utility namespace for working with malli schemas." + "Utility namespace for working with malli schemas, including a default registry and predicates used across Fabricate's implementation." {:reference "https://github.com/metosin/malli"} (:require [malli.core :as m] [malli.util :as mu] @@ -9,7 +9,7 @@ [babashka.fs :as fs])) -(def registry (atom {})) +(def registry "Global registry for Fabricate's Malli schemas." (atom {})) (mr/set-default-registry! (mr/composite-registry (m/default-schemas) (mt/schemas) @@ -19,7 +19,7 @@ (defn malli? - "Returns true if the given form is a valid malli schema" + "Returns true if the given form is a valid malli schema." {:malli/schema (m/schema [:=> [:cat :any] :boolean])} [form] (or (m/schema? form) @@ -34,6 +34,7 @@ (swap! registry assoc type schema)) (defn file? + "Returns true if the given value is a type that represents a file." {:malli/schema [:=> [:cat :any] :boolean]} [v] (or (instance? java.io.File v) (instance? java.nio.file.Path v))) @@ -41,6 +42,7 @@ (register! :file [:fn file?]) (defn ns? + "Returns true if the given value is a Namespace or is a symbol that names a namespace." {:malli/schema [:=> [:cat :any] :boolean]} [v] (or (instance? clojure.lang.Namespace v) (some? find-ns v))) @@ -73,11 +75,11 @@ (m/schema [:schema props new-ref]))) (def regex - "Malli schema for regular expressions" + "Malli schema for regular expressions." (m/schema [:fn #(instance? java.util.regex.Pattern %)])) (defn ns-form? - "Returns true if the given form is a valid Clojure (ns ...) special form" + "Returns true if the given form is a valid Clojure (ns ...) special form." {:malli/schema (m/schema [:=> [:cat :any] :boolean])} [form] (let [form @@ -92,7 +94,7 @@ (defn unify "A lighter-weight version of malli's own unify/merge that's - more compatible with number-based indexing/item access" + more compatible with number-based indexing/item access." {:malli/schema (m/schema [:=> [:cat [:* [:fn malli?]]] [:fn malli?]])} [schemas] (m/into-schema :orn {} (map-indexed vector schemas))) @@ -106,4 +108,5 @@ [:data {:optional true} :any]]]] [:trace [:vector :any]]])) (def ^{:malli/schema [:=> [:cat :any] :boolean]} throwable-map? + "Returns true if the given map matches the type returned by Throwable->map." (m/validator throwable-map-schema)) diff --git a/test/site/fabricate/api_test.clj b/test/site/fabricate/api_test.clj index f71a7f1..bfa6dab 100644 --- a/test/site/fabricate/api_test.clj +++ b/test/site/fabricate/api_test.clj @@ -4,6 +4,11 @@ ;; refer the dev ns to ensure multimethods have impls [site.fabricate.dev.build] [site.fabricate.prototype.test-utils :refer [with-instrumentation]] + [site.fabricate.prototype.read] + [site.fabricate.prototype.html] + [site.fabricate.prototype.hiccup] + [site.fabricate.prototype.check] + [site.fabricate.prototype.source.clojure] [malli.core :as m] [malli.util :as mu] [babashka.fs :as fs])) @@ -250,3 +255,31 @@ (t/testing (str v) (let [var-schema (:malli/schema (meta v))] (t/is (mu/equals schema var-schema) "API contract must be stable."))))) + +(defn test-namespace + [nmspc] + (let [ns-str (str nmspc) + publics (ns-publics nmspc)] + (t/testing ns-str + (let [ns-meta (meta nmspc)] + (t/is (string? (:doc ns-meta)) "Namespace should be documented") + (doseq [[sym v] publics] + (let [v-meta (meta v)] + (t/is (string? (:doc v-meta)) (str v " should be documented")))))))) + +(t/deftest documentation + (t/testing "documentation across APIs:" + (->> (all-ns) + (filter (fn [nmspc] + (let [ns-str (str nmspc)] + (and (re-find #"^site\.fabricate" ns-str) + (not (re-find #"^site\.fabricate\.adorn" ns-str)) + (not (re-find #"^site\.fabricate\.dev" ns-str)) + (not (re-find #"^site\.fabricate.*test" ns-str)) + (not (re-find #"^site\.fabricate.*docs" ns-str)) + (not (re-find #"^site\.fabricate.*time" ns-str)))))) + (run! test-namespace)))) + +(comment + (meta (find-ns 'site.fabricate.api)) + (ns-publics (find-ns 'site.fabricate.prototype.schema))) diff --git a/test/site/fabricate/prototype/check.clj b/test/site/fabricate/prototype/check.clj index c186563..a65224a 100644 --- a/test/site/fabricate/prototype/check.clj +++ b/test/site/fabricate/prototype/check.clj @@ -1,34 +1,25 @@ (ns site.fabricate.prototype.check - (:require [clojure.java.io :as io]) + "Namespace to check output HTML against validator.nu HTML tests." + (:require [clojure.java.io :as io] + [babashka.fs :as fs]) (:import [nu.validator.validation SimpleDocumentValidator] [nu.validator.messages TextMessageEmitter MessageEmitterAdapter] [nu.validator.source SourceCode] [nu.validator.xml SystemErrErrorHandler])) -(def sax-error (SystemErrErrorHandler.)) - (def validator - (let [v (SimpleDocumentValidator.)] + "Default HTML Validator object." + (let [v (SimpleDocumentValidator.) + sys-error (SystemErrErrorHandler.)] (doto v - (.setUpMainSchema "http://s.validator.nu/html5-rdfalite.rnc" sax-error) - (.setUpValidatorAndParsers sax-error true false)))) + (.setUpMainSchema "http://s.validator.nu/html5-rdfalite.rnc" sys-error) + (.setUpValidatorAndParsers sys-error true false)))) (defn html + "Test the given directories and files" [{:keys [dirs files]}] (let [all-files (->> dirs - (map #(file-seq (io/file %))) - flatten - (concat (map io/file files)) - (filter #(.endsWith (.toString %) ".html")))] + (mapcat #(fs/glob % "**.html")) + (map fs/file) + (into (map fs/file files)))] (doseq [f all-files] (.checkHtmlFile validator f true)))) - -(comment - (.exists (io/file "/home/andrew/repos_main/fabricate/docs/index.html")) - (.checkHtmlFile validator - (io/file "/home/andrew/repos_main/fabricate/docs/index.html") - false) - (.checkHtmlFile validator - (io/file - "/home/andrew/repos_main/fabricate/docs/fabricate.html") - false) - (html {:dirs ["docs/"]})) diff --git a/test/site/fabricate/prototype/source/clojure_test.clj b/test/site/fabricate/prototype/source/clojure_test.clj index 0ffefad..fb48d72 100644 --- a/test/site/fabricate/prototype/source/clojure_test.clj +++ b/test/site/fabricate/prototype/source/clojure_test.clj @@ -2,7 +2,7 @@ (:require [clojure.test :as t] [babashka.fs :as fs] [malli.core :as m] - [site.fabricate.prototype.document.clojure :as clj])) + [site.fabricate.prototype.source.clojure :as clj])) (def valid-form-map? (m/validator clj/form-map-schema))