Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add finding docs functionality from ClojureDocs #64

Merged
merged 2 commits into from
Jul 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* [#46](https://github.com/clojure-emacs/orchard/pull/46): [Inspector] Show fields inherited from superclasses when rendering raw objects.
* [#47](https://github.com/clojure-emacs/orchard/pull/46): [Java] Cache class-info for editable Java classes.
* [#51](https://github.com/clojure-emacs/orchard/issues/51): Add basic xref functionality in `orchard.xref`.
* [#64](https://github.com/clojure-emacs/orchard/issues/64): Port `cache-dir` from [soc/directories-jvm](https://github.com/soc/directories-jvm) to `orchard.os`.
* [#64](https://github.com/clojure-emacs/orchard/issues/64): Add finding docs functionality from ClojureDocs in `orchard.clojuredocs`.

### Changes

Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ TEST_SELECTOR := :java$(JAVA_VERSION)

TEST_PROFILES := +test

test:
test-resources/clojuredocs/export.edn:
curl -o $@ https://clojuredocs-edn.netlify.com/export.edn

test: test-resources/clojuredocs/export.edn
lein with-profile +$(VERSION),$(TEST_PROFILES) test $(TEST_SELECTOR)

test-watch:
test-watch: test-resources/clojuredocs/export.edn
lein with-profile +$(VERSION),$(TEST_PROFILES) test-refresh $(TEST_SELECTOR)

# Eastwood can't handle orchard.java.legacy-parser at the moment, because
Expand Down
85 changes: 85 additions & 0 deletions src/orchard/clojuredocs.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
(ns orchard.clojuredocs
"Find docs from ClojureDocs and retrieve the result as a map."
{:author "Masashi Iizuka"
:added "0.5.0"}
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]
[orchard.os :as os])
(:import
(java.time Instant)))

(def cache (atom {}))
(def default-edn-file-url
"https://clojuredocs-edn.netlify.com/export.edn")
(def cache-file-name
(str/join os/file-separator [(os/cache-dir)
"orchard"
"clojuredocs"
"export.edn"]))
(def ^{:doc "One week. Unit is millisecond."}
cache-updating-threshold 604800000)

(defn- write-cache-file! [url]
(.. (io/file cache-file-name)
getParentFile
mkdirs)
(->> url slurp (spit cache-file-name)))

(defn- map-from-key [f coll]
(reduce #(assoc %1 (f %2) %2) {} coll))

(defn- load-cache-file! [cache-file]
(let [docs (-> cache-file slurp edn/read-string)]
(->> (:vars docs)
(group-by #(:ns %))
(reduce-kv #(assoc %1 %2 (map-from-key :name %3)) {})
(reset! cache))
true))

(defn load-cache!
"Load exported documents file from ClojureDocs, and store it as a cache.
A EDN format file is expected to the `export-edn-url` argument.

If `export-edn-url` is omitted, `default-edn-file-url` is used.

The loaded EDN file will be cached in `cache-file-name`.
If the cached file is older than `cache-updating-threshold`,
the cached file will be updated automatically."
{:added "0.5.0"}
([]
(load-cache! default-edn-file-url))
([export-edn-url]
(let [cache-file (io/file cache-file-name)
now-milli (-> (Instant/now) .getEpochSecond (* 1000))]
(cond
(or (not (.exists cache-file))
(>= (- now-milli (.lastModified cache-file))
cache-updating-threshold))
(do (write-cache-file! export-edn-url)
(load-cache-file! cache-file))

(empty? @cache)
(load-cache-file! cache-file)))))

(defn clean-cache!
"Clean a cached file and documents"
{:added "0.5.0"}
[]
(.delete (io/file cache-file-name))
(reset! cache {}))

(defn find-doc
"Find a document matching to ns-name and var-name from cached documents.
Cache will be updated when there are no cached documents or cached documents are old.

If `export-edn-url` is omitted, `default-edn-file-url` is used.

Return nil if there is no matching document."
{:added "0.5.0"}
([ns-name var-name]
(find-doc ns-name var-name default-edn-file-url))
([ns-name var-name export-edn-url]
(load-cache! export-edn-url)
(get-in @cache [ns-name var-name])))
80 changes: 80 additions & 0 deletions src/orchard/os.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
(ns orchard.os
"Operating system specific utilities.
This is a port of BaseDirectories.java in soc/directories-jvm.
https://github.com/soc/directories-jvm"
{:author "Masashi Iizuka"
:added "0.5.0"}
(:require [clojure.java.io :as io]
[clojure.string :as str])
(:import (java.io BufferedReader)))

(def os-name
(str/lower-case (System/getProperty "os.name")))

(def os-type
(condp #(str/includes? %2 %1) os-name
"linux" ::linux
"mac" ::mac
"windows" ::windows
"bsd" ::bsd
::not-supported))

(def file-separator (System/getProperty "file.separator"))

(defn- run-commands
[expected-result-lines commands]
(let [commands ^"[Ljava.lang.String;" (into-array String commands)
builder (ProcessBuilder. commands)
process (.start builder)]
(with-open [reader ^BufferedReader (io/reader (.getInputStream process))]
(try
(doall (repeatedly expected-result-lines #(.readLine reader)))
(finally
(.destroy process))))))

(defn- get-win-dirs
[guids]
(let [commands (concat ["& {"
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
"Add-Type @\\\""
"using System;"
"using System.Runtime.InteropServices;"
"public class Dir {"
" [DllImport(\\\"shell32.dll\\\")]"
" private static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr pszPath);"
" public static string GetKnownFolderPath(string rfid) {"
" IntPtr pszPath;"
" if (SHGetKnownFolderPath(new Guid(rfid), 0, IntPtr.Zero, out pszPath) != 0) return \\\"\\\";"
" string path = Marshal.PtrToStringUni(pszPath);"
" Marshal.FreeCoTaskMem(pszPath);"
" return path;"
" }"
"}"
"\\\"@"]
(map #(str "[Dir]::GetKnownFolderPath(\\\"" % "\\\")") guids)
["}"])]
(run-commands (count guids) ["powershell.exe" "-Command" (str/join "\n" commands)])))

(defn cache-dir
"Returns the path to the user's cache directory.

macOS : $HOME/Library/Caches
Windows: FOLDERID_LocalAppData\\cache
Others : $XDG_CACHE_HOME or $HOME/.cache"
{:added "0.5.0"}
[]
(case os-type
::mac
(str/join file-separator [(System/getProperty "user.home")
"Library"
"Caches"])

::windows
(-> ["F1B32785-6FBA-4FCF-9D55-7B8E7F157091"]
get-win-dirs
first)

(let [cache-home (System/getenv "XDG_CACHE_HOME")]
(if (str/blank? cache-home)
(str (System/getProperty "user.home") file-separator ".cache")
cache-home))))
1 change: 1 addition & 0 deletions test-resources/clojuredocs/export.edn

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions test/orchard/clojuredocs_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
(ns orchard.clojuredocs-test
(:require
[clojure.java.io :as io]
[clojure.test :as test :refer [deftest is testing use-fixtures]]
[orchard.clojuredocs :as docs])
(:import
(java.time Instant)))

(def ^:private test-edn-file
(io/resource "clojuredocs/export.edn"))

(defn- create-dummy-cache-file [& [timestamp]]
(doto (io/file docs/cache-file-name)
(spit (slurp test-edn-file))
(cond-> timestamp (.setLastModified timestamp))))

(defn- clojuredocs-test-fixture [f]
(with-redefs [docs/cache-file-name "target/clojuredocs/export.edn"]
(docs/clean-cache!)
(f)
(docs/clean-cache!)))

(use-fixtures :each clojuredocs-test-fixture)

(deftest load-cache!-test
(let [cache-file (io/file docs/cache-file-name)
now (-> (Instant/now) .getEpochSecond (* 1000))
new-timestamp (- now (/ docs/cache-updating-threshold 2))
old-timestamp (- now docs/cache-updating-threshold)]
(testing "No cache-file"
(is (not (.exists cache-file)))
(is (empty? @docs/cache))
(docs/load-cache! test-edn-file)
(is (.exists cache-file))
(is (not (empty? @docs/cache))))

(testing "Old cache-file"
(create-dummy-cache-file old-timestamp)
(reset! docs/cache {:dummy "not-empty-dummy-data"})

(is (= old-timestamp (.lastModified cache-file)))
(is (contains? @docs/cache :dummy))
(docs/load-cache! test-edn-file)
(is (< old-timestamp (.lastModified cache-file)))
(is (not (contains? @docs/cache :dummy))))

(testing "Sufficiently new cache-file and no cached documents"
(create-dummy-cache-file new-timestamp)
(reset! docs/cache {})

(is (= new-timestamp (.lastModified cache-file)))
(is (empty? @docs/cache))
(docs/load-cache! test-edn-file)
(is (= new-timestamp (.lastModified cache-file)))
(is (not (empty? @docs/cache))))

(testing "Sufficiently new cache file and already cached documents"
(create-dummy-cache-file new-timestamp)
(reset! docs/cache {:dummy "not-empty-dummy-data"})

(is (= new-timestamp (.lastModified cache-file)))
(is (contains? @docs/cache :dummy))
(docs/load-cache! test-edn-file) ; Does nothing
(is (= new-timestamp (.lastModified cache-file)))
(is (contains? @docs/cache :dummy)))))

(deftest clean-cache!-test
(create-dummy-cache-file)
(reset! docs/cache {:dummy "not-empty-dummy-data"})
(let [cache-file (io/file docs/cache-file-name)]
(is (.exists cache-file))
(is (not (empty? @docs/cache)))
(docs/clean-cache!)
(is (not (.exists cache-file)))
(is (empty? @docs/cache))))

(deftest find-doc-test
(testing "find existing document"
(is (empty? @docs/cache))
(is (not (.exists (io/file docs/cache-file-name))))
(let [result (docs/find-doc "clojure.core" "first" test-edn-file)]
(is (map? result))
(is (every? #(contains? result %)
[:arglists :doc :examples :name :notes :ns :see-alsos]))
(is (.exists (io/file docs/cache-file-name)))
(is (not (empty? @docs/cache)))))

(testing "find non-existing document"
(is (nil? (docs/find-doc test-edn-file "non-existing-ns" "non-existing-var")))))
23 changes: 23 additions & 0 deletions test/orchard/os_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns orchard.os-test
(:require
[clojure.string :as str]
[clojure.test :as test :refer [deftest is testing]]
[orchard.os :as os]))

(deftest cache-dir-test
(testing "BSD"
(with-redefs [os/os-type ::os/bsd]
(is (str/ends-with? (os/cache-dir) "/.cache"))))

(testing "Linux"
(with-redefs [os/os-type ::os/linux]
(is (str/ends-with? (os/cache-dir) "/.cache"))))

(testing "Mac"
(with-redefs [os/os-type ::os/mac]
(is (str/ends-with? (os/cache-dir) "/Library/Caches")))))

;; NOTE: This test case targets AppVeyor mainly
(deftest cache-dir-windows-test
(when (= ::os/windows os/os-type)
(is (re-seq #"^C:\\Users\\.+\\AppData\\Local" (os/cache-dir)))))
9 changes: 6 additions & 3 deletions test/orchard/resource_test.clj
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
(ns orchard.resource-test
(:require
[clojure.test :refer [deftest testing is]]
[clojure.string :as str]
[clojure.test :refer [deftest is testing]]
[orchard.resource :as resource]))

(deftest resource-path-tuple-test
(is (nil? (resource/resource-path-tuple "jar:file:fake.jar!/fake/file.clj"))))

(deftest project-resources-test
(testing "get the correct resources for the orchard project"
(is (= "see-also.edn" (-> (resource/project-resources) first :relpath)))
(is (= java.net.URL (-> (resource/project-resources) first :url class)))))
(let [resources (->> (resource/project-resources)
(remove #(str/ends-with? (.getAbsolutePath (:root %)) "test-resources")))]
(is (= "see-also.edn" (-> resources first :relpath)))
(is (= java.net.URL (-> resources first :url class))))))