diff --git a/src/orchard/spec.clj b/src/orchard/spec.clj index 154d6965..a585d2a2 100644 --- a/src/orchard/spec.clj +++ b/src/orchard/spec.clj @@ -41,22 +41,43 @@ (str %)) form)) +(defn- ns-name->ns-alias + "Return mapping from full namespace name to its alias in the given namespace." + [^String ns] + (if ns + (reduce-kv (fn [m alias ns] + (assoc m (name (ns-name ns)) (name alias))) + {} + (ns-aliases (symbol ns))) + {})) + (defn spec-list "Retrieves a list of all specs in the registry, sorted by ns/name. If filter-regex is not empty, keep only the specs with that prefix." - [filter-regex] - (let [sorted-specs (->> (registry) - keys - (map str) - sort)] - (if (not-empty filter-regex) - (filter (fn [spec-symbol-str] - (let [checkable-part (if (.startsWith ^String spec-symbol-str ":") - (subs spec-symbol-str 1) - spec-symbol-str)] - (re-find (re-pattern filter-regex) checkable-part))) - sorted-specs) - sorted-specs))) + ([filter-regex] + (spec-list filter-regex nil)) + ([filter-regex ns] + (let [ns-alias (ns-name->ns-alias ns) + sorted-specs (->> (registry) + keys + (mapcat (fn [kw] + ;; Return an aliased entry in the current ns (if any) + ;; with the fully qualified keyword + (let [keyword-ns (namespace kw)] + (if (= ns keyword-ns) + [(str kw) (str "::" (name kw))] + (if-let [alias (ns-alias keyword-ns)] + [(str kw) (str "::" alias "/" (name kw))] + [(str kw)]))))) + sort)] + (if (not-empty filter-regex) + (filter (fn [spec-symbol-str] + (let [checkable-part (if (.startsWith ^String spec-symbol-str ":") + (subs spec-symbol-str 1) + spec-symbol-str)] + (re-find (re-pattern filter-regex) checkable-part))) + sorted-specs) + sorted-specs)))) (defn get-multi-spec-sub-specs "Given a multi-spec form, call its multi method methods to retrieve @@ -109,15 +130,37 @@ form)) sub-form)) +(defn- expand-ns-alias + "Expand a possible ns aliased keyword into a fully qualified keyword." + [^String ns ^String spec-name] + (if (and ns (.startsWith spec-name "::")) + (let [slash (.indexOf spec-name "/")] + (if (= -1 slash) + ;; This is a keyword in the current namespace + (str ":" ns "/" (subs spec-name 2)) + + ;; This is a keyword in an aliased namespace + (let [[keyword-ns kw] (.split (subs spec-name 2) "/") + aliases (ns-aliases (symbol ns)) + ns-name (some-> keyword-ns symbol aliases ns-name name)] + (if ns-name + (str ":" ns-name "/" kw) + spec-name)))) + + ;; Nothing to expand + spec-name)) + (defn spec-form "Given a spec symbol as a string, get the spec form and prepare it for a response." - [spec-name] - (when-let [spec (spec-from-string spec-name)] - (-> (form spec) - add-multi-specs - normalize-spec-form - str-non-colls))) + ([spec-name] + (spec-form spec-name nil)) + ([spec-name ns] + (when-let [spec (spec-from-string (expand-ns-alias ns spec-name))] + (-> (form spec) + add-multi-specs + normalize-spec-form + str-non-colls)))) (defn spec-example "Given a spec symbol as a string, returns a string with a pretty printed diff --git a/test/orchard/spec_test.clj b/test/orchard/spec_test.clj index 71ace829..c01217ad 100644 --- a/test/orchard/spec_test.clj +++ b/test/orchard/spec_test.clj @@ -20,3 +20,35 @@ (clojure.core/fn [%] (clojure.core/< (:start %) (:end %)))) :ret (clojure.core/fn [%] (clojure.core/> (:start %) (:end %))) :fn nil))))) + +(def spec-available? (or (resolve (symbol "clojure.spec.alpha" "get-spec")) + (resolve (symbol "clojure.spec" "get-spec")))) + +(when spec-available? + (deftest spec-is-found-by-ns-alias + (testing "current ns keyword" + (testing "spec-list finds current ns keyword" + (eval '(clojure.spec.alpha/def ::foo string?)) + (let [specs (into #{} + (spec/spec-list "" "orchard.spec-test"))] + (is (specs "::foo") "Spec is found with current ns") + (is (specs ":orchard.spec-test/foo") "Spec is found with fully qualified name"))) + + (testing "spec-form finds current ns keyword" + (let [spec1 (spec/spec-form "::foo" "orchard.spec-test") + spec2 (spec/spec-form ":orchard.spec-test/foo" "orchard.spec-test")] + (is (= "clojure.core/string?" spec1 spec2) "Both return the same correct spec")))) + + (testing "ns aliased keyword" + (eval '(clojure.spec.alpha/def :orchard.spec/test-dummy boolean?)) + (testing "spec-list finds keyword in aliased namespace" + + (let [specs (into #{} + (spec/spec-list "" "orchard.spec-test"))] + (is (specs "::spec/test-dummy") "Spec is found with ns-aliased keyword") + (is (specs ":orchard.spec/test-dummy") "Spec is found with fully qualified name"))) + + (testing "spec-form finds keyword in aliased namespace" + (let [spec1 (spec/spec-form "::spec/test-dummy" "orchard.spec-test") + spec2 (spec/spec-form ":orchard.spec/test-dummy" "orchard.spec-test")] + (is (= "clojure.core/boolean?" spec1 spec2) "Both return the same correct spec"))))))