Skip to content

Commit

Permalink
0.8.0 some API changes; add optional specs (draft)
Browse files Browse the repository at this point in the history
Cleans up the relationship between FW/1 and Ring. The Ring request is
now embedded directly in the RC, along with the event data. There is no
longer an intermediate request key in the RC.
  • Loading branch information
seancorfield committed Oct 20, 2016
1 parent b8f2327 commit 6ffe738
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 69 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down
21 changes: 6 additions & 15 deletions build.boot
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(def project 'framework-one)
(def version "0.7.5")
(def version "0.8.0")

(task-options!
pom {:project project
Expand All @@ -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
Expand All @@ -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)))
Expand All @@ -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)))
94 changes: 43 additions & 51 deletions src/framework/one.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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."
Expand All @@ -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."
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions src/framework/one/spec.clj
Original file line number Diff line number Diff line change
@@ -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?)

0 comments on commit 6ffe738

Please sign in to comment.