From d57db3257b32291b134fbd7e7b1072c314af0596 Mon Sep 17 00:00:00 2001 From: Daniel Pittman Date: Tue, 6 Sep 2011 23:45:54 -0700 Subject: [PATCH] Add `defhandler` to underlay `defsimplehandler`... `defhandler` wraps the complexity of building a handler, together with default behaviour, in a simple macro that expands, more or less, to define a type, augment the default behaviours with the supplied behaviours, and then creates a ready-to-use instance for the user. `defsimplehandler` is reimplemented in terms of `defhandler`, and the complex server test is also implemented using the wrapper macro. Finally, update the documentation to reflect these changes. Signed-off-by: Daniel Pittman --- README.markdown | 144 ++++++++++++++++++---------- src/clothesline/service/helpers.clj | 45 +++++++-- test/clothesline/complex_server.clj | 78 +++++++-------- 3 files changed, 163 insertions(+), 104 deletions(-) diff --git a/README.markdown b/README.markdown index 39abea2..9fcf967 100644 --- a/README.markdown +++ b/README.markdown @@ -39,10 +39,11 @@ web frameworks for content delivery, is superb for designing RESTful interfaces without having to worry about correctness. BankSimple's stack is also multi-lingual, using Scala, Clojure, and -JRuby. It's important for our development efforts to have a plays-well-with-others project where code can be shared between languages. We think that JVM language crosstalk is going to -be a major asset for us, and increasingly you see other -companies talking about similar experiments. Maybe we're on to -something. Clothesline is a way of finding out. +JRuby. It's important for our development efforts to have a +plays-well-with-others project where code can be shared between languages. We +think that JVM language crosstalk is going to be a major asset for us, and +increasingly you see other companies talking about similar experiments. Maybe +we're on to something. Clothesline is a way of finding out. ### A Simple Example ### @@ -55,17 +56,19 @@ quickly make a simple hello-world service: ;; A default handler that only cares about content-types. ;; - ;; This not only defines a type, but actually instantiates - ;; example1-server. defsimplehandler is not meant for anything - ;; but the simplest use. + ;; This not only defines a type, but creates a named instance + ;; of the object matching the handler name, ready to be used + ;; as part of routing. It is not, however, a classical ring + ;; handler - only a Clothesline handler. (defsimplehandler example1-simple "text/plain" (fn [request graphdata] "Hello World.")) ;; Request is the ring request, passed through. - ;; graphdata is the accumulated data about the response. + ;; Graphdata is the accumulated data about the response. (defsimplehandler example1-params "text/plain" (fn [request graphdata] (str "Your params: " (:params request)))) + ;; A traditional clout routing table. Note the colon-params in the ;; service are provided and placed in (def routes {"/" example1-simple, "/:gratis" example1-params}) @@ -74,9 +77,44 @@ quickly make a simple hello-world service: (defonce *server* (produce-server routes {:port 9999 :join? false})) -`defsimplehandler` is actually a very simple macro. It expands our form -to the relatively simple handler form that overrides -`content-types-provided` for that specific instance. +`defsimplehandler` is actually a very simple macro. It expands our form to the +relatively simple handler form that overrides `content-types-provided` for +that specific instance. + +### A Not-Quite-So-Simple Example ### + +`defsimplehandler` is built on the `defhandler` macro, which wraps the +standard behaviour of building a new handler instance with the default +behaviours in place. A more complex resource is easy to build: + + (ns example1 + (:use clothesline.core + [clothesline.service.helpers :only [defhandler]] + [clothesline.protocol.test-helpers :only [annotated-return]])) + + (defhandler hello + ;; any request without a "greet" parameter is malformed. + ;; also, augment graphdata with that greeting. + :malformed-request? (fn [_ {:keys params} _] + (if-let [greeting (params "greet")] + (annotated-return false {:annotate {:greet greeting}}) + true)) + ;; ... mostly defaults. + :resource-exists? (constantly true) + :allowed-methods (constantly #{:get}) + ;; ...and some content generation functions. + :content-types-provided + (constantly {"text/html" fancy-hello + "text/plain" (fn [_ _ graphdata] + (str "Hello, " (:greet graphdata)))})) + + ;; A traditional clout routing table. Note the colon-params in the + ;; service are provided and placed in + (def routes {"/:greet" hello}) + + ;; This is our server instance: + (defonce *server* + (produce-server routes {:port 9999 :join? false})) ## Format of a Handler ## @@ -95,28 +133,33 @@ which use the Erlang Process dictionary to accumulate state, Clothesline prefers using annotated return values to allow the accumulated state to be arbitrarily extended. To this end, if you wish to extend the "graphdata" (Clothesline's name for the extended state) -you should use the record class defined in -`clothesline.interop.nodetest`, TestResult. This class contains two -cells, one is the `:result` cell which should contain your normal -result value. The other is an `:annotations` cell, which should contain -a Map. The map respects two keys: - -* annotate: (should contain a dictionary with Clojure keyword - keys). Any key placed in this - dictionary will be carried over to the graphdata as request. See - later in the documentation for some keys of interest for annotation. -* headers: (should contain a dictionary of string to string). This - dictionary will be appended to the graphdata response headers - outside of the normal HTTP logic, in - `(:headers graphdata)`. The most common header values to insert are - responses like "Location". +you should use `clothesline.protocol.test-helpers/annotated-return` to +augment your response + +That expands to the record class defined in `clothesline.interop.nodetest`, +`TestResult`. This class contains two cells, one is the `:result` cell which +should contain your normal result value. The other is an `:annotations` cell, +which should contain a Map. The map respects two keys: + +* `annotate`: (should contain a dictionary with Clojure keyword keys). + Any key placed in this dictionary will be available in the graphdata + as subsequent handlers are called. Later in the documentation for + some keys of interest for annotation. + +* `headers`: (should contain a dictionary of string to string). + This dictionary will be appended to the final set of response headers, + in addition to the normal HTTP logic. The most common header to insert + here would be `"Location"`. ----- -Please note that some common headers such as Content-Length and -Content-Type should be automatically generated for you, unless your -handler is unusual. Content-Length, in particular, can be disastrous -to modify since most browsers hang when confronted with an -over-large Content-Length header. + +Please note that some common headers such as `Content-Length`, and +`Content-Type` should be automatically generated for you, unless your handler +is *extremely* unusual. `Content-Length`, in particular, can be disastrous to +modify since most browsers hang when confronted with an over-large +`Content-Length` header. + +Augment, don't replace, standard HTTP headers in your handlers. ----- @@ -143,12 +186,12 @@ higher code re-usability and a cleaner, clearer architecture. There are a few key departures from WebMachine's model that should be noted. The most obvious is the content-types-provided and -content-types-accepted. These are maps of content-type-string to -function, but the functions are different. They *must* take two -arguments: the ring request and the current graphdata. The *must* -return a simple string or a function that evaluates to a simple string. -There are plans to allow for other return types (in particular: threads, streams, delay and future objects, -etc), but they are currently not supported. +content-types-accepted. These are maps of content-type-string to function, but +the functions are different. They *must* take two arguments: the ring request +and the current graphdata. The *must* return a simple string or a function +that evaluates to a simple string. There are plans to allow for other return +types (in particular: threads, streams, delay and future objects, etc), but +they are currently not supported. `allowed-methods` should return a Set as opposed to a List. @@ -160,13 +203,12 @@ etc), but they are currently not supported. Annotation keys are stored in the graphdata structure, which is passed amongst states and passed to every handler test. -The graphdata structure contain annotations and the sum of the headers -that should be explicitly added. These values can be directly -specified with annotations. If a test called later in the graph specifies a value that -contradicts an earlier value, the later specification overrides the -earlier one. It is important to note that these values are special, -but not the only allowed values. *Any key and value is a valid -annotation!* +The graphdata structure contain annotations and the sum of the headers that +should be explicitly added. These values can be directly specified with +annotations. If a test called later in the graph specifies a value that +contradicts an earlier value, the later specification overrides the earlier +one. It is important to note that these values are special, but not the only +allowed values. *Any key and value is a valid annotation!* `:headers` This is a string-string map of header values. Please note that headers are case-sensitive. The headers map is used by the graph @@ -187,13 +229,14 @@ used. ## Further Work Towards Completeness ## -* There are some outstanding issues the Accept header. If you're having problems with spurious 204s on clients, advise them to set their "Accept" header to exactly the content type they want for now. +* There are some outstanding issues the Accept header. If you're having + problems with spurious 204s on clients, advise them to set their "Accept" + header to exactly the content type they want for now. -* Currently, date-related states in the HTTP graph do not work -properly. +* Currently, date-related states in the HTTP graph do not work properly. * Encoding and charset changes also do not work correctly. All charsets -should be utf-8 for now. + should be utf-8 for now. * Data from `content-types-provided` and `content-types-accepted` is not checked during header generation. @@ -224,7 +267,6 @@ default behaviors that the default protocol provides. ## Installation -You struggle through for now with a hand-managed jar. Soon we'll have -a BankSimple open source Maven Repo and we'll make sure to have an -entry in Clojars. +You can grab ClothesLine from [CloJars](http://clojars.org/search?q=clothesline) +using your favorite tool. Leinengein works well enough. diff --git a/src/clothesline/service/helpers.clj b/src/clothesline/service/helpers.clj index e01baca..3146492 100644 --- a/src/clothesline/service/helpers.clj +++ b/src/clothesline/service/helpers.clj @@ -1,8 +1,12 @@ (ns clothesline.service.helpers (:require [clothesline.service :as service])) - -(def ^{:arglists '([type map-of-fns] [type & kw-and-impls]) :name "extend-as-handler"} +(def + ^{:name "extend-as-handler" + :arglists '([type map-of-fns] [type & kw-and-impls]) + :doc "Extend an existing type with the service behaviours to become a +Clothesline resource. This merges the supplied behaviours with the default +set of behaviours required to work correctly."} extend-as-handler (fn [type & params] (if (and (map? (first params)) @@ -11,13 +15,36 @@ (extend type service/service (merge service/service-default (apply hash-map params)))))) -(defmacro defsimplehandler [name & ct-generator-forms] +(defmacro defhandler + "Define a new Clothesline handler type and instance, extending the default +behaviour with custom handlers by mapping symbols to service implementations. + +See `clothesline.service/service` or WebMachine for available methods, +including their arguments and return values. + +See `extend-as-handler` for full details of the valid body forms. + + (defhandler example {:service-available? (constantly false)}) + + ;; (defn sample-malformed-request [...] ...) + (defhandler sample + :allowed-methods (constantly #{:head :get}) + :malformed-request? sample-malformed-request? + :resource-exists? (fn [...] ...))" + {:arglists '([name map-of-fns] [name & kw-and-impls])} + [name & params] (let [typename (symbol (str name "-type"))] `(do + ;; Define a type to hook into the protocol mechanism. (deftype ~typename []) - (extend ~typename service/service - (merge service/service-default - {:content-types-provided - (fn [handler# request# graphdata#] (hash-map ~@ct-generator-forms)) - :allowed-methods (constantly #{:get :head :post :options :put})})) - (def ~name (new ~typename))))) \ No newline at end of file + ;; Extend that with the standard suite of behaviours... + (clothesline.service.helpers/extend-as-handler ~typename ~@params) + ;; ...and a user accessor. + (def ~name (new ~typename))))) + + +(defmacro defsimplehandler [name & ct-generator-forms] + `(clothesline.service.helpers/defhandler ~name + :allowed-methods (constantly #{:get :head :post :put :delete :options}) + :content-types-provided (fn [handler# request# graphdata#] + (hash-map ~@ct-generator-forms)))) diff --git a/test/clothesline/complex_server.clj b/test/clothesline/complex_server.clj index 3587cc6..4d3c3ad 100644 --- a/test/clothesline/complex_server.clj +++ b/test/clothesline/complex_server.clj @@ -14,49 +14,39 @@ ;; Behavior -(def behavior - {:allowed-methods (fn [_ _ _] - (test/annotated-return #{:get :post :put} - {:annotate {:debug-output - (fn [_] (println "o/~"))}})) - - :malformed-request? (fn [_ {:keys [request-method params]} _] - (let [name (params "name") - location (params "location")] - (cond - (= request-method :get) (nil? name) - :otherwise (or (nil? name) - (nil? location))))) - :previously-existed? (fn [_ {params :params} _] (get-name-url (params "name"))) - :resource-exists? (constantly false) - :allow-missing-post? (constantly true) - :moved-permanently? (fn [_ {params :params method :request-method} _] - - (if (= method :put) - false - (get-name-url (params "name")))) - :post-is-create? (fn [_ {params :params} _] (not (name-exists? (params "name")))) - :create-path (fn [_ {{:strs [name]} :params} _] (test/annotated-return (str "/" name))) - :process-post (fn [_ {{:strs [name location]} :params :as request} _] - (add-name-url name location) - (test/annotated-return true)) - - ;; This is mostly just to handle params - :content-types-accepted (fn [& _] - {"*/*" (fn [{params :params body :body} _] - (add-name-url (params "name") - (params "location")))}) - } - -) - -(defrecord bookmark-handler []) -(helpers/extend-as-handler bookmark-handler behavior) +(helpers/defhandler bookmark-handler + :allowed-methods (fn [_ _ _] + (test/annotated-return #{:get :post :put} + {:annotate {:debug-output + (fn [_] (println "o/~"))}})) + :malformed-request? (fn [_ {:keys [request-method params]} _] + (let [name (params "name") + location (params "location")] + (cond + (= request-method :get) (nil? name) + :otherwise (or (nil? name) + (nil? location))))) + :previously-existed? (fn [_ {params :params} _] (get-name-url (params "name"))) + :resource-exists? (constantly false) + :allow-missing-post? (constantly true) + :moved-permanently? (fn [_ {params :params method :request-method} _] + (if (= method :put) + false + (get-name-url (params "name")))) + :post-is-create? (fn [_ {params :params} _] (not (name-exists? (params "name")))) + :create-path (fn [_ {{:strs [name]} :params} _] (test/annotated-return (str "/" name))) + :process-post (fn [_ {{:strs [name location]} :params :as request} _] + (add-name-url name location) + (test/annotated-return true)) + + ;; This is mostly just to handle params + :content-types-accepted (fn [& _] + {"*/*" (fn [{params :params body :body} _] + (add-name-url (params "name") + (params "location")))})) ;; Server - - -(defonce *server* (delay (clothesline.core/produce-server {"/:name" (bookmark-handler.)} - {:join? false :port 9001}))) - - +(defonce *server* + (delay (clothesline.core/produce-server + {"/:name" bookmark-handler} + {:join? false :port 9001})))