Common ground for different modelling libs (Spec, Schema, ...) for web usage.
- for Schema, there are multiple ways to add extra metadata (at least in ring-swagger, schema-tools and in compojure-api)
- emerging tools for spec
- could have a common model
- less syntax to learn
- easy to transition in between
(require '[schema.core :as s])
;; a schema
s/Int
;; a schema record
(st/schema s/Int)
;; with extra meta-data
(st/schema
s/Int
{:description "it's an int"})
;; alternative syntax
(st/schema
{:schema s/Int
:description "it's an int"})
(require '[spec-tools.core :as st])
;; a spec
int?
;; a spec record
(st/spec int?)
;; with extra meta-data
(st/spec
int?
{:description "it's an int"})
;; alternative syntax
(st/spec
{:spec int?
:description "it's an int"})
;; with type hint
(st/spec
{:spec int?
:spec/type :int
:description "it's an int"})
Same model for different libs. Common structure for errors (with :type
identifier :schema
or :spec
).
- has inbuilt selective runtime coercion via matchers for different wire-formats, e.g.
schema.coerce/json-coercion-matcher
schema.coerce/string-coercion-matcher
(require '[schema.core :as s])
(require '[schema.coerce :as sc])
(s/defschema User
{:name s/Str
:type (s/enum :grunt :boss)})
(def json->User (sc/coercer User sc/json-coercion-matcher))
(def ->User (sc/coercer User (constantly nil)))
(->User {:name "Tiina", :type :boss})
; {:name "Tiina", :type :boss}
(->User {:name "Tiina", :type "boss"})
; #schema.utils.ErrorContainer{:error {:type (not (#{:boss :grunt} "boss"))}}
(json->User {:name "Tiina", :type "boss"})
; {:name "Tiina", :type :boss}
- has not, but spec-tools provides these, e.g.
spec-tools.core/json-conformers
spec-tools.core/string-conformers
(s/def ::name st/string?)
(s/def ::type (s/and st/keyword? #{:grunt :boss}))
(s/def ::user (s/keys :req-un [::name ::type]))
(def json->user #(st/conform ::user % st/json-conformers))
(def ->user #(st/conform ::user % (constantly nil)))
(->user {:name "Tiina", :type :boss})
; {:name "Tiina", :type :boss}
(->user {:name "Tiina", :type "boss"})
; :clojure.spec/invalid
(json->user {:name "Tiina", :type "boss"})
; {:name "Tiina", :type :boss}
Clojure routing libs should be able emit a openapi3-formatted data: routes, endpoints and operations. Web-libs can add more keys for their own use. Use the common-meta-data model here.
(require '[clj-open-api.schema :as open-api])
(require '[schema.tools.core :as st])
(require '[schema.core :as s])
(s/defeschema Person
{::id s/Int
:name (st/schema s/Str {:description "Name of a user"})
:age (st/schema s/Int {:description "Age of a user"})})
(open-api/create-spec
{:paths
{"/echo-person"
{:post
{:summary "Echoes a person"
:parameters {:body Person}
:responses {200 {:schema Person}}}}}})
(require '[clj-open-api.spec :as open-api])
(require '[spec.tools.core :as st])
(require '[clojure.spec :as s])
(s/def ::id int?)
(s/def ::name (st/spec string? {:description "Name of a user"}))
(s/def ::age (st/spec int? {:description "Age of a user"}))
(s/def ::person (st/spec (s/keys :req [::id] ::req-un [::name ::age])))
(open-api/create-spec
{:paths
{"/echo-person"
{:post
{:summary "Echoes a person"
:parameters {:body ::person}
:responses {200 {:schema ::person}}}}}})
- same data models could be used for middleware (and interceptors) to describe their requirements for request & response.
- Kekkonen (https://github.com/metosin/kekkonen#basic-building-blocks) does this already: the Interceptor input & output schemas are merged into the endpoint combined schema
- Interceptors would look ~like this:
;; interceptor loading an user, requiring an `s/Int` path-parameter
(interceptor
{:name ::load-user
:input {:type :schema
:request {:path-params {:id s/Int}}}
:enter (fn [context]
(let [db (-> context ::db)
user-id (-> context :request :path-params :id)
user (load-user db user-id)]
(assoc context ::user user)))})
- Middleware could use
- meta-data in Macchiato
- there could be new
ring.middleware/Middleware
executable record, less opaque.
;; async-middleware loading an user, requiring an `s/Int` path-parameter
(middleware
{:name ::load-user
:input {:type :schema
:request {:path-params {:id s/Int}}}
:handle (fn [handler]
(fn [request respond raise]
(let [db (-> request ::db)
user-id (-> request :path-params :id)
user (load-user db user-id)]
(handler (assoc request ::user user) respond raise))))})