aboutsummaryrefslogtreecommitdiff
path: root/transition/lib.clj
blob: 03111c19c1ca213822a8b05ef5d009212661c3cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
;;  Copyright (c) Stephen C. Gilardi. All rights reserved. The use and
;;  distribution terms for this software are covered by the Common Public
;;  License 1.0 (http://opensource.org/licenses/cpl.php) which can be found
;;  in the file CPL.TXT at the root of this distribution. By using this
;;  software in any fashion, you are agreeing to be bound by the terms of
;;  this license. You must not remove this notice, or any other, from this
;;  software.
;;
;;  Libs
;;
;;  A 'lib' is a named set of resources in classpath whose contents define
;;  a library of Clojure code. Lib names are symbols and each lib is
;;  associated with a Clojure namespace and a Java package that share its
;;  name. A lib's name also locates its root directory within classpath
;;  using Java's package name to classpath-relative path mapping. All
;;  resources in a lib should be contained in the directory structure under
;;  its root directory. All definitions a lib makes should be in its
;;  associated namespace.
;;
;;  Clojure loads a lib by loading its root resource. The root resource
;;  path is derived from the root directory path by repeating its last
;;  component and appending '.clj'. For example, the lib 'x.y.z has root
;;  directory <classpath>/x/y/z; root resource <classpath>/x/y/z/z.clj.
;;  The root resource should contain code to create the lib's namespace and
;;  load any additional lib resources.
;;
;;  Clojure provides functions to:
;;
;;    - ensure libs are loaded while avoiding duplicate loads (require)
;;    - require libs and refer to their namespaces (use)
;;    - return a sorted set of symbols naming loaded libs (loaded-libs)
;;    - load Clojure code from resources (load-resources)
;;
;;  Libspecs
;;
;;  A libspec identifies a lib to load. It's either a lib name or a vector
;;  containing a lib name followed by options expressed as squential
;;  keywords and arguments. Libspec examples:
;;
;;    - 'clojure.contrib.sql
;;    - '[clojure.contrib.sql :exclude (get-connection) :as sql]
;;
;;  Prefix Lists
;;
;;  It's common for Clojure code to depend on several libs whose names have
;;  the same prefix. When specifying libs, prefix lists can be used to
;;  reduce repetition. A prefix list contains the shared prefix followed by
;;  libspecs with the shared prefix removed from the lib names. After
;;  removing the prefix, the names that remain must not contain any
;;  periods. For example, the following are equivalent:
;;
;;    - (require 'clojure.contrib.def '[clojure.contrib.sql :as db])
;;    - (require '(clojure.contrib def [sql :as db]))
;;
;;  Examples
;;
;;    (require '(clojure.contrib sql sql.tests))
;;    (use '(clojure.contrib sql ns-utils) :verbose)
;;
;;    (use :reload-all :verbose
;;      '(clojure.contrib
;;        [sql :exclude (get-connection)
;;             :rename {execute-commands do-commands}]
;;        [ns-utils :as nsu]))
;;
;;  scgilardi (gmail)
;;  Created 7 April 2008
;;
;;  Thanks to Stuart Sierra for providing many useful ideas, discussions
;;  and code contributions for lib.clj.

(clojure/in-ns 'clojure)

;; Private

(defmacro init-once
  "Initializes a var exactly once. The var must already exist."
  {:private true}
  [var init]
  `(let [v# (resolve '~var)]
     (when-not (.isBound v#)
       (.bindRoot v# ~init))))

(def
 #^{:private true :doc
    "A ref to a sorted set of symbols representing loaded libs"}
 *loaded-libs*)
(init-once *loaded-libs* (ref (sorted-set)))

(def
 #^{:private true :doc
    "True while a verbose load is pending"}
 *loading-verbosely*)
(init-once *loading-verbosely* false)

(defn- format
  "Formats a string using String/format, see java.util.Formatter for format
  string syntax"
  [fmt & args]
  (String/format fmt (to-array args)))

(defn- printf
  "Prints formatted output"
  [fmt & args]
  (print (apply format fmt args)))

(defn- throw-if
  "Throws an exception with a message if pred is true"
  [pred fmt & args]
  (when pred
    (let [message (apply format fmt args)
          exception (Exception. message)
          raw-trace (.getStackTrace exception)
          boring? #(not= (.getMethodName %) "doInvoke")
          trace (into-array (drop 2 (drop-while boring? raw-trace)))]
      (.setStackTrace exception trace)
      (throw exception))))

(defn- libspec?
  "Returns true if x is a libspec"
  [x]
  (or (symbol? x)
      (and (vector? x)
           (or
            (nil? (second x))
            (keyword? (second x))))))

(defn- prepend
  "Prepends a symbol or a seq to coll"
  [x coll]
  (if (symbol? x)
    (cons x coll)
    (concat x coll)))

(defn- root-directory
  "Returns the root directory path for a lib"
  [sym]
  (str \/
       (.. (name sym)
           (replace \- \_)
           (replace \. \/))))

(defn- root-resource
  "Returns the root resource path for a lib"
  [sym]
  (let [d (root-directory sym)
        i (inc (.lastIndexOf d (int \/)))
        leaf (.substring d i)]
    (str d \/ leaf ".clj")))

(def load-resources)

(defn- load-one
  "Loads a lib given its name. If need-ns, ensures that the associated
  namespace exists after loading. If require, records the load so any
  duplicate loads can be skipped."
  [sym need-ns require]
  (load-resources (root-resource sym))
  (throw-if (and need-ns (not (find-ns sym)))
            "namespace '%s' not found after loading '%s'"
            sym (root-resource sym))
  (when require
    (dosync
     (commute *loaded-libs* conj sym))))

(defn- load-all
  "Loads a lib given its name and forces a load of any libs it directly or
  indirectly loads. If need-ns, ensures that the associated namespace
  exists after loading. If require, records the load so any duplicate loads
  can be skipped."
  [sym need-ns require]
  (dosync
   (commute *loaded-libs* clojure.set/union
            (binding [*loaded-libs* (ref (sorted-set))]
              (load-one sym need-ns require)
              @*loaded-libs*))))

(defn- load-lib
  "Loads a lib with options"
  [prefix sym & options]
  (throw-if (and prefix (pos? (.indexOf (name sym) (int \.))))
            "lib names inside prefix lists must not contain periods")
  (let [sym (if prefix (symbol (str prefix \. sym)) sym)
        opts (apply hash-map options)
        as (:as opts)
        reload (:reload opts)
        reload-all (:reload-all opts)
        require (:require opts)
        use (:use opts)
        verbose (:verbose opts)
        loaded (contains? @*loaded-libs* sym)
        load (cond reload-all
                   load-all
                   (or reload (not require) (not loaded))
                   load-one)
        need-ns (or as use)
        filter-opts (select-keys opts '(:exclude :only :rename))]
    (binding [*loading-verbosely* (or *loading-verbosely* verbose)]
      (if load
        (load sym need-ns require)
        (throw-if (and need-ns (not (find-ns sym)))
                  "namespace '%s' not found" sym))
      (when (and need-ns *loading-verbosely*)
        (printf "(clojure/in-ns '%s)\n" (ns-name *ns*)))
      (when as
        (when *loading-verbosely*
          (printf "(clojure/alias '%s '%s)\n" as sym))
        (alias as sym))
      (when use
        (when *loading-verbosely*
          (printf "(clojure/refer '%s" sym)
          (doseq opt filter-opts
            (printf " %s '%s" (key opt) (print-str (val opt))))
          (printf ")\n"))
        (apply refer sym (mapcat seq filter-opts))))))

(defn- load-libs
  "Loads libs, interpreting prefix lists and libspecs for forwarding to
  load-lib"
  [& args]
  (let [flags (filter keyword? args)
        opts (interleave flags (repeat true))
        args (filter (complement keyword?) args)]
    (doseq arg args
      (if (libspec? arg)
        (apply load-lib nil (prepend arg opts))
        (let [[prefix & args] arg]
          (throw-if (nil? prefix) "prefix cannot be nil")
          (doseq arg args
            (apply load-lib prefix (prepend arg opts))))))))

;; Public

(defn require
  "Loads libs, skipping any that are already loaded. Each argument is
  either a libspec that identifies a lib, a prefix list that identifies
  multiple libs whose names share a common prefix, or a flag that modifies
  how all the identified libs are loaded.

  Libs

  A 'lib' is a named set of resources in classpath whose contents define a
  library of Clojure code. Lib names are symbols and each lib is associated
  with a Clojure namespace and a Java package that share its name. A lib's
  name also locates its root directory within classpath using Java's
  package name to classpath-relative path mapping. All resources in a lib
  should be contained in the directory structure under its root directory.
  All definitions a lib makes should be in its associated namespace.

  'require loads a lib by loading its root resource. The root resource path
  is derived from the root directory path by repeating its last component
  and appending '.clj'. For example, the lib 'x.y.z has root directory
  <classpath>/x/y/z; root resource <classpath>/x/y/z/z.clj. The root
  resource should contain code to create the lib's namespace and load any
  additional lib resources.

  Libspecs

  A libspec is a lib name or a vector containing a lib name followed by
  options expressed as sequential keywords and arguments.

  Recognized options: :as
  :as takes a symbol as its argument and makes that symbol an alias to the
    lib's namespace in the current namespace.

  Prefix Lists

  It's common for Clojure code to depend on several libs whose names have
  the same prefix. When specifying libs, prefix lists can be used to reduce
  repetition. A prefix list contains the shared prefix followed by libspecs
  with the shared prefix removed from the lib names. After removing the
  prefix, the names that remain must not contain any periods.

  Flags

  A flag is a keyword.
  Recognized flags: :reload, :reload-all, :verbose
  :reload forces loading of all the identified libs even if they are
    already loaded
  :reload-all implies :reload and also forces loading of all libs that the
    identified libs directly or indirectly load via require or use
  :verbose triggers printing information about calls to load-resources,
    alias, and refer"
  [& args]
  (apply load-libs :require args))

(defn use
  "Like 'require, but also refers to each lib's namespace using
  clojure/refer.

  'use accepts additional options in libspecs: :exclude, :only, :rename.
  The arguments and semantics for :exclude, :only, and :rename are the same
  as those documented for clojure/refer."
  [& args]
  (apply load-libs :require :use args))

(defn loaded-libs
  "Returns a sorted set of symbols naming the currently loaded libs"
  []
  @*loaded-libs*)

(defn load-resources
  "Loads Clojure code from resources in classpath. A path is interpreted as
  classpath-relative if it begins with a slash or relative to the root
  directory for the current namespace otherwise."
  [& paths]
  (doseq path paths
    (let [path (if (.startsWith path "/")
                 path
                 (str (root-directory (ns-name *ns*)) \/ path))]
      (when *loading-verbosely*
        (printf "(clojure/load-resources \"%s\")\n" path)
        (flush))
      (.loadResourceScript clojure.lang.RT (.substring path 1)))))