diff --git a/README.md b/README.md index 539c788..1c2eeb5 100644 --- a/README.md +++ b/README.md @@ -110,16 +110,16 @@ Any controller function also has access to the the FW/1 API (after `require`ing * `(cookie rc name value)` - sets `name` to `value` in the cookie scope, and returns the updated `rc`. * `(event rc name)` - returns the value of `name` from the event scope (`:action`, `:section`, `:item`, or `:config`). * `(event rc name value)` - sets `name` to `value` in the event scope, and returns the updated `rc`. -* `(flash rc name)` - returns the value of `name` from the flash scope. * `(flash rc name value)` - sets `name` to `value` in the flash scope, and returns the updated `rc`. * `(header rc name)` - return the value of the `name` HTTP header, or `nil` if no such header exists. * `(header rc name value)` - sets the `name` HTTP header to `value` for the response, and returns the updated `rc`. +* `(parameters rc)` - returns just the form / URL parameters from the request context (plus whatever parameters have been added by controllers). This is useful when you want to iterate over the data elements without worrying about any of the 'special' data that FW/1 puts in `rc`. * `(redirect rc url)` or `(redirect rc status url)` - returns `rc` containing information to indicate a redirect to the specified `url`. * `(reload? rc)` - returns `true` if the current request includes URL parameters to force an application reload. * `(remote-addr rc)` - returns the IP address of the remote requestor (if available). * `(render-xxx rc data)` or `(render-xxx rc status data)` - render the specified `data`, optionally with the specified `status` code, in format _xxx_: `html`, `json`, `raw-json`, `text`, `xml`. -* `(ring rc name)` - returns the specified element of the original Ring request. -* `(servlet-request rc)` - returns a "fake" `HttpServletRequest` object that delegates `getParameter` calls to pull data out of `rc`, as well as implementing several other calls (to the Ring request data); used for interop with other HTTP-centric libraries. +* `(ring rc)` - returns the original Ring request. +* `(servlet-request rc)` - returns a "fake" `HttpServletRequest` object that delegates `getParameter` calls to pull data out of `rc`, as well as implementing several other calls (delegating to the Ring request data); used for interop with other HTTP-centric libraries. * `(session rc name)` - returns the value of `name` from the session scope. * `(session rc name value)` - sets `name` to `value` in the session scope, and returns the updated `rc`. * `(to-long val)` - converts `val` to a long, returning zero if it cannot be converted (values in `rc` come in as strings so this is useful when you need a number instead and zero can be a sentinel for "no value"). diff --git a/build.boot b/build.boot index 0817e51..a72fef4 100644 --- a/build.boot +++ b/build.boot @@ -1,5 +1,5 @@ (def project 'framework-one) -(def version "0.7.5") +(def version "0.8.0") (task-options! pom {:project project @@ -11,7 +11,8 @@ "http://www.eclipse.org/legal/epl-v10.html"}}) (set-env! :resource-paths #{"src"} - :dependencies '[[org.clojure/clojure "1.8.0" :scope "provided"] + :source-paths #{"test"} + :dependencies '[[org.clojure/clojure "RELEASE" :scope "provided"] ; render as xml [org.clojure/data.xml "0.1.0-beta2"] ; render as JSON @@ -26,9 +27,10 @@ ; standardized routing [compojure "1.6.0-beta1"] [http-kit "2.2.0" :scope "test"] - [seancorfield/boot-expectations "RELEASE" :scope "test"]]) + [seancorfield/boot-expectations "RELEASE" :scope "test"] + [org.clojure/test.check "RELEASE" :scope "test"]]) -(require '[seancorfield.boot-expectations :refer [expectations]]) +(require '[seancorfield.boot-expectations :refer [expectations expecting]]) (deftask build [] (comp (pom) (jar) (install))) @@ -50,14 +52,3 @@ (require '[usermanager.main :as app]) ((resolve 'app/-main) port server) identity))) - -(deftask with-test [] - (merge-env! :source-paths #{"test"} - :dependencies '[[expectations "RELEASE"]]) - identity) - -(ns-unmap *ns* 'test) - -(deftask test [] - (comp (with-test) - (expectations :verbose true))) diff --git a/src/framework/one.clj b/src/framework/one.clj index 2b1af05..4709d3c 100644 --- a/src/framework/one.clj +++ b/src/framework/one.clj @@ -38,12 +38,6 @@ (meta #'selmer.filters/add-filter!)) (deref #'selmer.filters/add-filter!)) -;; scope access utility -(defn scope-access [scope] - (fn - ([rc n] (get-in rc [::request scope n])) - ([rc n v] (assoc-in rc [::request scope n] v)))) - ;; render data support (defn render-data ([rc as expr] @@ -56,36 +50,42 @@ ;; (fw1/default-handler) - returns Ring middleware for your application ;; See the bottom of this file for more details -(def cookie +(defn cookie "Get / set items in cookie scope: (cookie rc name) - returns the named cookie (cookie rc name value) - sets the named cookie" - (scope-access :cookies)) + ([rc name] (get-in rc [::ring :cookies name])) + ([rc name value] (assoc-in rc [::ring :cookies name] value))) -(def event +(defn event "Get / set FW/1's 'event' scope data. Valid event scope entries are: - :action :section :item :config - You should normally only read the event data: (event rc key)" - (scope-access ::event)) + :action :section :item :config and :headers (response headers) + (event rc name) - returns the named event data + (event rc name value) - sets the named event data (internal use only!)" + ([rc name] (get-in rc [::event name])) + ([rc name value] (assoc-in rc [::event name] value))) -(def flash +(defn flash "Get / set items in 'flash' scope. Data stored in flash scope in a request should be automatically restored to the 'rc' on the - subsequent request. You should not need to read flash scope, - just store items there: (flash rc name value)" - (scope-access :flash)) - -(def ring - "Get data from the original Ring request -- not really intended for - public usage, but may be useful to some applications." - (scope-access ::ring)) + subsequent request. + (flash rc name value)" + ([rc name value] (assoc-in rc [::ring :flash name] value))) (defn header "Either read the request headers or write the response headers: (header rc name) - return the named (request) header (header rc name value) - write the named (response) header" - ([rc n] (get-in rc [::request :req-headers n])) - ([rc n v] (assoc-in rc [::request :headers n] v))) + ([rc n] (get-in rc [::ring :headers n])) + ([rc n v] (assoc-in rc [::event :headers n] v))) + +(defn parameters + "Return just the parameters portion of the request context, without + the Ring request and event data special keys. This should be used + when you need to iterate over the form/URL scope elements of the + request context without needing to worry about special keys." + [rc] + (dissoc rc ::event ::ring)) (defn redirect "Tell FW/1 to perform a redirect." @@ -108,7 +108,7 @@ This value comes directly from Ring and is dependent on your application server (so it may be IPv4 or IPv6)." [rc] - (get-in rc [::request :remote-addr])) + (get-in rc [::ring :remote-addr])) (defn render-html "Tell FW/1 to render this expression (string) as-is as HTML." @@ -146,28 +146,33 @@ ([rc status expr] (render-data rc status :xml expr))) +(defn ring + "Get data from the original Ring request -- not really intended for + public usage, but may be useful to some applications. + (ring rc) - returns the whole Ring request" + ([rc] (get rc ::ring))) + (defn servlet-request "Return a fake HttpServletRequest that knows how to delegate to the rc." [rc] (proxy [javax.servlet.http.HttpServletRequest] [] (getContentType [] - (let [headers (ring rc :headers)] - (get headers "content-type"))) + (get-in (ring rc) [:headers "content-type"])) (getHeader [name] - (let [headers (ring rc :headers)] - (get headers (str/lower-case name)))) + (get-in (ring rc) [:headers (str/lower-case name)])) (getMethod [] - (str/upper-case (name (ring rc :request-method)))) + (-> (ring rc) :request-method name str/upper-case)) (getParameter [name] (if-let [v (get rc (keyword name))] (str v) nil)))) -(def session +(defn session "Get / set items in session scope: (session rc name) - returns the named session variable (session rc name value) - sets the named session variable Session variables persist across requests and use Ring's session middleware (and can be memory or cookie-based at the moment)." - (scope-access :session)) + ([rc name] (get-in rc [::ring :session name])) + ([rc name value] (assoc-in rc [::ring :session name] value))) (defn to-long "Given a string, convert it to a long (or zero if it is not @@ -348,29 +353,16 @@ nil))) (defn pack-request - "Given a request context and a Ring request, return the request context with certain - Ring data embedded in it. In particular, we keep request headers separate to any - response headers (and merge those in unpack-response below)." + "Given a request context and a Ring request, return the request context with + the Ring data embedded in it, and the 'flash' scope merged." [rc req] - (merge - (reduce (fn [m k] - (assoc-in m - [::request (if (= :headers k) :req-headers k)] - (or (k req) {}))) - (assoc-in rc [::request ::ring] req) - [:session :cookies :remote-addr :headers]) - (:flash req))) + (merge (assoc rc ::ring req) (:flash req))) (defn unpack-response - "Given a request context and a response, return the response with Ring data added. - By this point the response always has headers so we must add to those, not overwrite." + "Given a request context (returned by controllers) and a response map (status, + headers, body), return a full Ring response map." [rc resp] - (reduce (fn [m k] - (if (= :headers k) - (update m k merge (get-in rc [::request k])) - (assoc m k (get-in rc [::request k])))) - resp - [:session :cookies :flash :headers])) + (merge (ring rc) (update resp :headers merge (event rc :headers)))) (defn render-request "Given the application configuration, the specific section, item, and a (Ring) @@ -453,7 +445,7 @@ :reload :reload :reload-application-on-every-request false :suffix "html" ; views / layouts would be .html - :version "0.7.3"}) + :version "0.8.0"}) (defn- build-config "Given a 'public' application configuration, return the fully built diff --git a/src/framework/one/spec.clj b/src/framework/one/spec.clj new file mode 100644 index 0000000..83afac3 --- /dev/null +++ b/src/framework/one/spec.clj @@ -0,0 +1,76 @@ +;; Framework One (FW/1) Copyright (c) 2016 Sean Corfield +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +(ns framework.one.spec + (:require [clojure.spec :as s])) + +(alias 'fw1 (create-ns 'framework.one)) + +;; FW/1 request context always has event data and a Ring request +;; and may have any arbitrary unqualified keys which represent the +;; form and URL parameters for a request (and any data added by a +;; controller, available for the views). +;; In addition, a controller may choose to redirect or render data. + +(s/def ::fw1/rc (s/keys :req [::fw1/event ::fw1/ring] + :opt [::fw1/redirect ::fw1/render])) + +(s/def ::fw1/event (s/keys :req-un [::action ::section ::item ::config] + :opt-un [::headers])) + +(s/def ::action string?) +(s/def ::section string?) +(s/def ::item string?) +(s/def ::config (s/map-of keyword? any?)) + +(s/def ::fw1/redirect (s/keys :req-un [::status :location/headers])) +(s/def :location/headers (s/map-of #{"Location"} string?)) + +(s/def ::fw1/render (s/keys :req-un [::as ::data] :opt-un [::status])) +(s/def ::as #{:html :json :raw-json :text :xml}) +(s/def ::data any?) + +;; basic Ring request + +(s/def ::fw1/ring (s/keys :req-un [::headers ::protocol ::remote-addr + ::request-method ::scheme + ::server-name ::server-port + ::uri] + :opt-un [::body + ::character-encoding + ::content-length ::content-type + ::query-string ::ssl-client-cert])) + +(s/def ::headers (s/map-of string? string?)) +(s/def ::protocol string?) +(s/def ::remote-addr string?) +(s/def ::request-method #{:delete :get :head :options :patch :post :put}) +(s/def ::scheme #{:http :https}) +(s/def ::server-name string?) +(s/def ::server-port pos-int?) +(s/def ::uri string?) + +(s/def ::body string?) ; mostly! +(s/def ::character-encoding string?) ; deprecated +(s/def ::content-length nat-int?) ; deprecated +(s/def ::content-type string?) ; deprecated +(s/def ::query-string string?) +(s/def ::ssl-client-cert any?) + +;; Ring response map (which is also FW/1's response map) + +(s/def ::fw1/response (s/keys :req-un [::headers ::status] + :opt-un [::body])) + +(s/def ::status pos-int?)