aboutsummaryrefslogtreecommitdiff
path: root/src/main/clojure/clojure/contrib/error_kit.clj
blob: 6cffd859c5bc0ed3f160052c9171a54797663fe4 (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
;   Copyright (c) Chris Houser, Jan 2009. All rights reserved.
;   The use and distribution terms for this software are covered by the
;   Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;   which can be found in the file epl-v10.html 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.

; == EXPERIMENTAL ==
; System for defining and using custom errors
; Please contact Chouser if you have any suggestions for better names
; or API adjustments.

(ns 
  ^{:author "Chris Houser",
     :doc "EXPERIMENTAL
System for defining and using custom errors
Please contact Chouser if you have any suggestions for better names
or API adjustments."} 
  clojure.contrib.error-kit
  (:use [clojure.contrib.def :only (defvar defvar-)]
        [clojure.stacktrace :only (root-cause)]))

(defn- make-ctrl-exception [msg data]
  "Create an exception object with associated data, used for passing
  control and data to a dynamically containing handler."
  (proxy [Error clojure.lang.IDeref] [msg]
    (toString [] (str "Error Kit Control Exception: " msg ", " (pr-str data)))
    (deref [] data)))

(defvar- ctrl-exception-class
  (class (make-ctrl-exception nil nil)))

(defvar- *handler-stack* () "Stack of bound handler symbols")

(defvar- *continues* {} "Map of currently available continue forms")


(defmacro throw-msg
  "Returns a function that throws a Java Exception with the given
  name.  Useful to associate a new error-kit error type with a
  particular Java Exception class, via the :unhandled error key."
  [class-name]
  `(fn [x#] (throw (new ~class-name (:msg x#)))))

(defn error
  "Base type for all error-kit errors"
  {::args [:msg :unhandled :tag]}
  [details]
  (merge {:tag `error :msg "exception via error-kit"
          :unhandled (throw-msg Exception)}
         details))

(defn- qualify-sym [sym]
  (let [v (resolve sym)]
    (assert v)
    (apply symbol (map #(str (% (meta v))) [:ns :name]))))

(defmacro deferror
  "Define a new error type"
  {:arglists '([name [parent-error?] doc-string? [args*] & body]
               [name [parent-error?] doc-string? args-destruct-map & body])}
  [err-name pvec & decl]
  (let [pvec (if (empty? pvec) [`error] pvec)
        [docstr args & body] (if (string? (first decl)) decl (cons nil decl))
        args (or args [])
        argmap (if (vector? args) `{:keys ~args} args)
        body (or body {})
        qual-err-name (symbol (str *ns*) (name err-name))]
    (assert (== (count pvec) 1)) ; only support single-inheritance for now
    (assert (vector? args)) ; only vector (keyword destruct) args for now
    `(do
       (defn ~err-name [details#]
         (let [basedata# ((resolve (first (parents '~qual-err-name))) details#)
               ~argmap basedata#]
           (merge basedata# {:tag '~qual-err-name} (do ~@body) details#)))
       (alter-meta! (var ~err-name) assoc
                    :doc ~docstr ::args ~(vec (map #(keyword (str %)) args)))
       ~@(for [parent pvec]
           `(derive '~qual-err-name '~(qualify-sym parent)))
       (var ~err-name))))

(defn- throw-to [msg target-map args]
  (throw (make-ctrl-exception msg (assoc target-map :args args))))

(defn raise*
  "Raise the given error object, best if created by an error
  constructor defined with deferror.  See also 'raise' macro."
  [err]
  (let [err-tag (:tag err)]
    (loop [hs *handler-stack*]
      (if (empty? hs)
        ((:unhandled err) err)
        (let [[{:keys [htag] :as handler}] hs]
          (if (and htag (not (isa? err-tag htag)))
            (recur (next hs))
            (let [rtn ((:hfunc handler) err)]
              (if-not (vector? rtn)
                (throw-to "default" handler (list rtn))
                (condp = (rtn 0)
                  ::continue-with (rtn 1)
                  ::continue (if-let [continue (*continues* (rtn 1))]
                               (throw-to "continue" continue (rtn 2))
                               (do (prn *continues*) (throw
                                 (Exception.
                                   (str "Unbound continue name " (rtn 1))))))
                  ::do-not-handle (recur (next hs))
                  (throw-to "do-not-handle" handler (list rtn)))))))))))

(defmacro raise
  "Raise an error of the type err-name, constructed with the given args"
  [err-name & args]
  `(raise* (~err-name ~(zipmap (::args (meta (resolve err-name)))
                               args))))

; It'd be nice to assert that these are used in a tail position of a handler
(defmacro do-not-handle
  "Use in a tail position of a 'handle' form to indicate 'raise' should
  not consider the error handled, but should continue searching for an
  appropriate 'handle' form.  Allows finer-grain control over catching
  than just the error type."
  []
  `[::do-not-handle])

(defmacro continue-with [value]
  "Use in a tail position of a 'handle' form to cause the currently
  running 'raise' to return the given 'value'."
  `[::continue-with ~value])

(defmacro continue [continue-name & args]
  "Use in a tail position of a 'handle' form to pass control to the
  named 'continue' form, passing in the given args.  The 'continue'
  form with the given name and the smallest dynamic scope surrounding
  the currently running 'raise' will be used."
  `[::continue '~continue-name [~@args]])


(def ^{:doc "Special form to be used inside a 'with-handler'.  When
  any error is 'raised' from withing the dynamic scope of 'body' that
  is of error-name's type or a derived type, the args will be bound
  and the body executed.  If no 'error-name' is given, the body will
  be executed for regardless of the type of error raised.  The body
  may return a value, in which case that will be the return value of
  the entire 'with-handler' form, or it may use any of the special
  return forms, 'do-not-handle', 'continue-with', or 'continue'."
          :arglists '([error-name? [args*] & body]
                      [error-name? args-destruct-map-args & body])}
  handle)

(def ^{:doc "Special form to be used inside a 'with-handler'.
  Control can be passed to this 'continue' form from a 'raise' enclosed
  in this with-handler's dynamic scope, when this 'continue-name' is
  given to a 'continue' form."
        :arglists '([continue-name [args*] & body])}
  bind-continue)

(defn- special-form [form]
  (and (list form)
       (symbol? (first form))
       (#{#'handle #'bind-continue} (resolve (first form)))))


(defmacro with-handler
  "This is error-kit's dynamic scope form.  The body will be executed
  in a dynamic context that includes all of the following 'handle' and
  'bind-continue' forms."
  [& forms]
  (let [[body special-forms] (split-with (complement special-form) forms)]
    (assert (every? special-form special-forms))
    (let [blockid (gensym)
          handlers (for [[type & more] special-forms
                         :when (= (resolve type) #'handle)]
                     (let [[htag args & hbody] (if (symbol? (first more))
                                                 more
                                                 (cons nil more))
                           argmap (if (vector? args) `{:keys ~args} args)]
                       `{:blockid '~blockid
                         :htag ~(when htag (list `quote (qualify-sym htag)))
                         :hfunc (fn [~argmap] ~@hbody)
                         :rfunc identity}))
          continues (into {}
                          (for [[type & more] special-forms
                                :when (= (resolve type) #'bind-continue)]
                            [(list `quote (first more))
                             `{:blockid '~blockid
                               :rfunc (fn ~@(next more))}]))]
      `(try
         (binding [*handler-stack* (list* ~@handlers @#'*handler-stack*)
                   *continues* (merge @#'*continues* ~@continues)]
           ~@body)
         (catch Throwable e#
           (let [root-cause# (root-cause e#)]
             (if-not (instance? @#'ctrl-exception-class root-cause#)
               (throw e#)
               (let [data# @root-cause#]
                 (if (= '~blockid (:blockid data#))
                   (apply (:rfunc data#) (:args data#))
                   (throw e#))))))))))

(defn rebind-fn [func]
  (let [a *handler-stack*, b *continues*]
    (fn [& args]
      (binding [*handler-stack* a
                *continues* b]
        (apply func args)))))

(comment

(alias 'kit 'clojure.contrib.error-kit)

; This defines an error and its action if unhandled.  A good choice of
; unhandled. action is to throw a Java exception so users of your code
; who do not want to use error-kit can still use normal Java try/catch
; forms to handle the error.
(kit/deferror number-error [] [n]
  {:msg (str "Number error: " n)
   :unhandled (kit/throw-msg NumberFormatException)})

(kit/deferror odd-number-error [number-error]
  "Indicates an odd number was given to an operation that is only
  defined for even numbers."
  [n]
  {:msg (str "Can't handle odd number: " n)})

; Raise an error by name with any extra args defined by the deferror
(defn int-half [i]
  (if (even? i)
    (quot i 2)
    (kit/raise odd-number-error i)))

; Throws Java NumberFormatException because there's no 'handle' form
(vec (map int-half [2 4 5 8]))

; Throws Java Exception with details provided by 'raise'
(kit/with-handler
  (vec (map int-half [2 4 5 8]))
  (kit/handle odd-number-error [n]
    (throw (Exception. (format "Odd number %d in vector." n)))))

; The above is equivalent to the more complicated version below:
(kit/with-handler
  (vec (map int-half [2 4 5 8]))
  (kit/handle {:keys [n tag]}
    (if (isa? tag `odd-number-error)
      (throw (Exception. (format "Odd number %d in vector." n)))
      (kit/do-not-handle))))

; Returns "invalid" string instead of a vector when an error is encountered
(kit/with-handler
  (vec (map int-half [2 4 5 8]))
  (kit/handle kit/error [n]
    "invalid"))

; Inserts a zero into the returned vector where there was an error, in
; this case [1 2 0 4]
(kit/with-handler
  (vec (map int-half [2 4 5 8]))
  (kit/handle number-error [n]
    (kit/continue-with 0)))

; Intermediate continue: [1 2 :oops 5 4]
(defn int-half-vec [s]
  (reduce (fn [v i]
            (kit/with-handler
              (conj v (int-half i))
              (kit/bind-continue instead-of-half [& instead-seq]
                (apply conj v instead-seq))))
    [] s))

(kit/with-handler
  (int-half-vec [2 4 5 8])
  (kit/handle number-error [n]
    (kit/continue instead-of-half :oops n)))

; Notes:

; It seems likely you'd want to convert a handle clause to
; bind-continue, since it would allow higher forms to request what you
; used to do by default.  Thus both should appear in the same
; with-handler form

; Should continue-names be namespace qualified, and therefore require
; pre-definition in some namespace?
; (kit/defcontinue skip-thing "docstring")

; Could add 'catch' for Java Exceptions and 'finally' support to
; with-handler forms.

)