Skip to content

Instantly share code, notes, and snippets.

@alexanderkiel
Last active April 8, 2018 20:06
Show Gist options
  • Save alexanderkiel/a1b583741008aa346733a85bda074725 to your computer and use it in GitHub Desktop.
Save alexanderkiel/a1b583741008aa346733a85bda074725 to your computer and use it in GitHub Desktop.
An attempt to write a ring spec in clojure.spec.
(ns ring.spec
(:require [clojure.java.io :as io]
[clojure.spec :as s]
[clojure.spec.gen :as gen]
[clojure.string :as str])
(:import [java.io File InputStream]))
;; ---- Internal Primitives ---------------------------------------------------
(def ^:private token-pattern #"[\!#$%&'*+-.^_`|0-9A-Za-z]+")
(defn special []
(s/gen #{\\ \! \# \$ \% \& \' \* \+ \- \. \^ \_ \`}))
(defn token []
(->> (gen/one-of [(gen/char-alphanumeric) (special)])
(gen/vector)
(gen/fmap str/join)))
(s/def ::token (s/with-gen (s/and string? #(re-matches token-pattern %))
token))
(def ^:private lc-token-pattern #"[\!#$%&'*+-.^_`|0-9a-z]+")
(defn char-lc-alphanumeric []
(gen/fmap char (gen/one-of [(gen/choose 48 57)
(gen/choose 97 122)])))
(defn lc-token []
(->> (gen/one-of [(char-lc-alphanumeric) (special)])
(gen/vector)
(gen/fmap str/join)))
(s/def ::lc-token (s/with-gen (s/and string? #(re-matches lc-token-pattern %))
lc-token))
;; ---- Foreign Primitives ----------------------------------------------------
(defn- input-stream [n]
(io/input-stream (byte-array n)))
(s/def ::io/input-stream (s/spec #(instance? InputStream %)
:gen #(gen/fmap input-stream (gen/choose 0 100))))
;; ---- Primitives ------------------------------------------------------------
(s/def :ring/server-port (s/with-gen (s/and nat-int? #(<= % 65535))
#(gen/choose 0x41 0x5a)))
(s/def :ring/server-name string?)
(s/def :ring/remote-addr string?)
;; The request URI, excluding the query string and the "?" separator.
;; Must start with "/".
(s/def :ring/uri (s/with-gen (s/and string? #(str/starts-with? % "/"))
#(gen/fmap (fn [s] (str "/" s)) (s/gen string?))))
(s/def :ring/query-string string?)
(s/def :ring/scheme #{:http :https})
(s/def :ring/request-method (s/spec keyword? :gen #(s/gen #{:get :head :options :put :post :delete})))
(s/def :ring/protocol (s/spec string? :gen #(s/gen #{"HTTP/1.1"})))
(s/def :ring/header-name ::token)
(s/def :ring/lower-case-header-name ::lc-token)
(s/def :ring.request/headers (s/map-of :ring/lower-case-header-name string?))
(s/def :ring.response/header-value
(s/with-gen (s/or :str string?
:seq (s/coll-of string? :gen-max 2))
#(s/gen string?)))
(s/def :ring.response/headers (s/map-of :ring/header-name :ring.response/header-value))
(s/def :ring.request/body ::io/input-stream)
(s/def :ring.response/body (s/with-gen
(s/or :str string?
:seq seq?
:file #(instance? File %)
:in ::io/input-stream)
#(s/gen string?)))
(s/def :ring/status (s/with-gen (s/and nat-int? #(<= 100 % 599))
#(gen/choose 100 599)))
;; ---- Request Map -----------------------------------------------------------
;; A request map is a Clojure map containing at least the following keys and
;; corresponding values:
(s/def :ring/request (s/keys :req-un [:ring/server-port
:ring/server-name
:ring/remote-addr
:ring/uri
:ring/scheme
:ring/request-method
:ring/protocol
:ring.request/headers]
:opt-un [:ring/query-string
:ring.request/body]))
;; ---- Response Map ----------------------------------------------------------
;; A response map is a Clojure map containing at least the following keys and
;; corresponding values:
(s/def :ring/response (s/keys :req-un [:ring/status
:ring.response/headers]
:opt-un [:ring.response/body]))
;; ---- Handlers --------------------------------------------------------------
;; Ring handlers constitute the core logic of the web application. Handlers are
;; implemented as Clojure functions that process a given request map to generate
;; and return a response map.
(s/def :ring/handler (s/fspec :args (s/cat :req :ring/request)
:ret :ring/response))
@ddossot
Copy link

ddossot commented Jun 21, 2016

A few comments:

  • Limiting request-method to a well-known set is problematic, IMO. What if people use :patch or :purge? Or whatever they may come up with.
  • Response header values can be strings or seq of strings.
  • Also, sorry for the noob question, but is nat-int a new addition in 1.9? Is it like pos?

@dchelimsky
Copy link

nat-int? is 0 or pos? - added in 1.9.0-alpha7

@alexanderkiel
Copy link
Author

@ddossot Thanks, I changed the :ring/request-method definition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment