Skip to content
This repository has been archived by the owner on Apr 2, 2020. It is now read-only.

Extending JSON-API compatibility #5

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,20 @@ Each operation expects its implementation to be a function with a ring request o
Argo expects as the result of these functions a map with the following keys:

* `:data`: The data which will be returned in the API response. This should be a map for a single resource (as implemented with `:get`) or a vector/sequence for `:find`.
- Optional data: `:resource-identifiers` You may _optionally_ include related resource identifier objects.
* `:errors`: A map of keywords and string values. Will be converted to the JSON API error format. Works well with Prismatic schema error types.
* `:status`: Use to override the status of responses. Defaults to 400 for error responses and 200 for valid responses.
* `:exclude-source`: Use this to exclude the source object as per the JSON API spec in error responses.
* `:count`: argo provides automatic generation of pagination links if using pagination for `:find`. Use `:count` to let argo know how many total objects exist when implementing pagination.
* `:included` You may _optionally_ include top-level, related resource objects.
- for example:
```clojure
{
:data {...}
:included {:heroes [{:id 1 :name "Jason"}]
:ally {:id 2 :name "Medea"}}
}
```

In most circumstances it will probably only be necessary to include either `:data` or `:errors`.

Expand Down Expand Up @@ -266,11 +276,25 @@ This should return the following reponse.
"hero": {
"links": {
"related": "/v1/achievements/1/hero"
}
}
},
"data": { "type": "heroes", "id": 1 }
},
},
"type": "achievements"
}
},
"included": [
{
"type": "heroes",
"id": "1",
"attributes": {
"created": "2017-02-12T00:09:59Z",
"name": "Jason"
},
"links": {
"self": "/v1/heroes/1"
}
}
]
}
```

Expand Down
5 changes: 3 additions & 2 deletions example/src/example/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
{:data (db/find-achievements)})

:get (fn [req]
{:data (db/get-achievement (parse-id (-> req :params :id)))})
{:data (db/get-achievement (parse-id (-> req :params :id)))
:included {:heroes (db/get-achievement-hero (parse-id (-> req :params :id)))}})

:create (fn [req]
(if-let [errors (s/check NewAchievement (:body req))]
Expand All @@ -63,7 +64,7 @@
:rels {:hero {:type :heroes
:foreign-key :hero
:get (fn [req]
{:data (db/get-achievement-hero (parse-id (-> req :params :id)))})}}})
{:data (db/get-achievement-hero (parse-id (-> req :params :id)))})}}})

(defapi api
{:base-url "/v1"
Expand Down
2 changes: 1 addition & 1 deletion example/src/example/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
(defn get-achievement-hero
[id]
(when (integer? id)
(let [q (str "SELECT heroes.id AS id, heroes.name AS name, heroes.created AS created "
(let [q (str "SELECT heroes.id AS id, heroes.name AS name, heroes.created AS created, heroes.birthplace AS birthplace "
"FROM heroes, achievements "
"WHERE achievements.id = ? AND heroes.id = achievements.id")]
(first (jdbc/query db [q id])))))
79 changes: 63 additions & 16 deletions src/argo/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@
(def base-url "")

(defn ok
[data & {:keys [status headers links meta]}]
[data & {:keys [status headers links meta included]}]
{:status (or status 200)
:headers (merge {"Content-Type" "application/vnd.api+json"} headers)
:body (merge {:data data} (when links {:links links}) (when meta {:meta meta}))})
:body (merge
{:data data}
(when links {:links links})
(when meta {:meta meta})
(when included {:included included}))})

(defn flatten-errors
([errors]
Expand Down Expand Up @@ -92,16 +96,47 @@
(merge (when (> offset 0)
{:first (gen-qs uri params-encoded 0 limit)}))))))

(comment (dissoc (apply dissoc x (map (fn [[k v]] (:foreign-key v)) rels)) primary-key))

(defn build-relationships
"builds json-api formatted map for relationships,
including resource identifier objecs if in x"
[rel-type x primary-key rels]
(apply merge
(map (fn [[rel-key v]]
{rel-key (merge
{:links {:related (str base-url "/" rel-type "/" (get x primary-key) "/" (name rel-key))}}
(when-let [data (rel-key (:resource-identifiers x))]
{:data data}))})
rels)))

(defn remove-unused-keys
[x primary-key rels]
(let [rel-keys (map (fn [[k v]] (:foreign-key v)) rels)
keys-to-remove (concat rel-keys [:resource-identifiers primary-key])]
(apply dissoc x keys-to-remove)))

(defn x-to-api
[type x primary-key & [rels]]
[typ x primary-key & [rels]]
(when x
(merge {:type type
(merge {:type typ
:id (str (get x primary-key))
:attributes (dissoc (apply dissoc x (map (fn [[k v]] (:foreign-key v)) rels)) primary-key)
:links {:self (str base-url "/" type "/" (get x primary-key))}}
(when rels {:relationships (apply merge (map (fn [[k v]]
{k {:links {:related (str base-url "/" type "/" (get x primary-key) "/" (name k))}}})
rels))}))))
:attributes (remove-unused-keys x primary-key rels)
:links {:self (str base-url "/" typ "/" (get x primary-key))}}
(when rels
{:relationships (build-relationships typ x primary-key rels)}))))

(defn build-included
"builds collection of resources to include in a response"
[included]
(when (not-empty included)
(->> included
(map (fn [[typ included-of-type]]
(when (coll? included-of-type)
(if (map? included-of-type)
[(x-to-api (name typ) included-of-type :id)]
(map (fn [include] (x-to-api (name typ) include :id)) included-of-type)))))
(reduce (fn [acc curr] (into acc curr))))))

(defn wrap-pagination
[default-limit max-limit]
Expand Down Expand Up @@ -217,12 +252,15 @@
exclude-source# :exclude-source
status# :status
total# :count
m# :meta} (~get-many ~req)
m# :meta
included# :included} (~get-many ~req)
pag# (assoc (:page ~req) :count total#)
links# (gen-pagination-links ~req pag#)]
(if errors#
(bad-req errors# :status status# :exclude-source exclude-source#)
(ok (map (fn [x#] (x-to-api ~typ x# ~primary-key ~rels)) data#) :links links# :meta m#)))))
(cond
errors# (bad-req errors# :status status# :exclude-source exclude-source#)
(:include (:params ~req)) (bad-req {:?include "include resources not supported"} :status 400 :exclude-source exclude-source#)
(nil? data#) (not-found)
:else (ok (map (fn [x#] (x-to-api ~typ x# ~primary-key ~rels)) data#) :links links# :meta m# :included (build-included included#))))))

~@(when create
`(:post (let [{data# :data
Expand All @@ -245,11 +283,13 @@
status# :status
exclude-source# :exclude-source
errors# :errors
m# :meta} (~get-one ~req)]
m# :meta
included# :included} (~get-one ~req)]
(cond
errors# (bad-req errors# :status status# :exclude-source exclude-source#)
(:include (:params ~req)) (bad-req {:?include "include resources not supported"} :status 400 :exclude-source exclude-source#)
(nil? data#) (not-found)
:else (ok (x-to-api ~typ data# ~primary-key ~rels) :meta m#)))))
:else (ok (x-to-api ~typ data# ~primary-key ~rels) :meta m# :included (build-included included#))))))

~@(when update
`(:patch (let [{data# :data
Expand Down Expand Up @@ -305,7 +345,14 @@
:meta ~m))
`((ok (x-to-api ~typ ~data ~primary-key ~relations) :meta ~m))))))))
~@(when create
`(:post (rel-req ~create ~req)))
`(:post (let [{data# :data
errors# :errors
exclude-source# :exclude-source
status# :status
m# :meta} (~create ~req)]
(if errors#
(bad-req errors# :status status# :exclude-source exclude-source#)
(ok (x-to-api ~typ data# ~primary-key ~rels) :status 201 :meta m#)))))

~@(when update
`(:patch (rel-req ~update ~req)))
Expand Down