When I have a definition of an API like this:
(POST* "/register" []
:body-params [username :- String,
password :- String,
name :- String]
(ok)))
what's the appropriate way of making name optional? Is it:
(POST* "/register" []
:body-params [username :- String,
password :- String,
{name :- String nil}]
(ok)))
As you know it uses letk plumbing notation and as far as I recall the syntax is correct but the default value should be consistent with the expected type so I'd say it should be "" rather than nil as (string? nil) => false
(POST* "/register" []
:body-params [username :- String,
password :- String,
{name :- String ""}]
(ok)))
Related
In the below example, how can I set a default value for the path parameter item-id?
(POST "/:id" [item-id]
:path-params [item-id :- Int]
:body [body Body]
:query-params [{item-name :- Str nil}]
:summary "Create or update a item."
(ok ...))
You should match the path-parameter name to the string placeholder. Path-params don't need defaults - if there is no path-parameter present, the route doesn't match. Here's a working example:
(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])
(require '[schema.core :as s])
(require '[muuntaja.core :as m])
(def app
(api
(POST "/:item-id" []
:path-params [item-id :- s/Int]
:query-params [{item-name :- s/Str nil}]
:summary "Create or update a item."
(ok {:item-id item-id
:item-name item-name}))))
(->> {:request-method :post
:uri "/123"
:query-params {"item-name" "kikka"}}
(app)
:body
(m/decode m/instance "application/json"))
; => {:item-name "kikka", :item-id 123}
The default value is used if no value is present in the URL for the parameter. Path parameters are made optional by appending a question mark (?) to the end of the parameter name. For example, id?. The difference between optional values and default route parameters is:
A route parameter with a default value always produces a value.
An optional parameter has a value only when a value is provided by the request URL.
Path parameters may have constraints that must match the route value bound from the URL. Adding : and constraint name after the route parameter name
I have something like this (GET "/photo/:id/tags/:tag-id/...")
and thus for every route inside that context I have to typecast these ids to Integer explicitly. Is there any way to achieve this automatically or have a common place to typecast ids instead of each controller's action?
As of Compojure 1.4.0, you can also supply coercion functions for parameters using the :<< keyword:
[x :<< as-int]
In the above case, the parameter x will be passed through the as-int function before being assigned. If any coercion function returns nil, then the coercion is considered to have failed, and the route will not match.
Example:
(defroutes app
(GET "/customers" [] customers)
(GET "/suppliers" [] suppliers)
(GET "/accounts" [] accounts)
(context "/statements" []
(GET "/" [] statements)
(GET "/:id" [id :<< as-int] (single-statement id))))
You might get this behaviour using compojure-api where you can specify schema types for the URL/query params as well as request body. For example:
(defapi app
(GET "/photo/:id" []
:path-params [id :- Long]
(ok {:message (str "Photo with ID " id)})))
By specifying [id :- Long] you ask to do a coercion of id path param to Long type.
In compojure-api I noticed this two ways of specifying the API of a resource:
(POST* "/register" []
:body [user UserRegistration]
(ok)))
and
(POST* "/register" []
:body-params [username :- String,
password :- String]
(ok)))
What are the difference between these two? What are the implications of using one vs the other?
The only difference is in how the params are specified (and destructured thereafter):
body:
reads body-params into a enhanced let. First parameter is the let
symbol, second is the Schema to coerced! against.
Example:
:body [user User]
body-params:
restructures body-params with plumbing letk notation.
Example: :body-params [id :- Long name :- String]
Depending on the situation you might prefer one or the other. In both cases the params (user in first case, id and name in the second) will be in scope for the body.
Cheshire's custom encoders seem suitable for this problem and I wrote a little helper function:
(defn add-rec-encoder [Rec type-token]
(add-encoder Rec
(fn [rec jg] (.writeString jg
(str (encode-map (assoc rec :type type-token) jg))))))
(defrecord A [a])
(add-rec-encoder A "A")
(encode (->A "abc"))
But it produces a strange trailing "".
=> {"a":"abc","type":"A"} ""
What is causing this? And is there another approach worth considering (I also need to be able to decode back to a record based on this type-token)?
(encode-map ... jg) directly writes the encoded map to the JSON generator jg, then returns nil.
This means that, your call to writeString is actually:
(.writeString jg (str nil))
which, since (str nil) is "", will encode and append exactly that to the JSON generator. The correct encoder logic would be:
(defn add-rec-encoder [Rec type-token]
(add-encoder Rec
(fn [rec jg]
(encode-map (assoc rec :type type-token) jg))))
I'm trying to annotate the Element record in clojure.data.xml which is:
(defrecord Element [tag attrs content])
I've annotated it as follows:
(t/ann-record Element [tag :- t/Keyword attrs :- (t/HMap :complete? false)
content :- (t/Vec Element)])
And I have the following function which doesn't type check:
(t/ann get-content [Element -> (t/Vec Element)])
(defn get-content [xml]
(:content xml))
with 'Expected: t/Vec clojure.data.xml.Element Actual: t/Any'
I've also tried replacing that with (get xml :content) but it fails with the same output.
I wonder what I'm doing wrong :D
It might be that you need to fully qualify Element in your ann-record call (I'm assuming your code is not actually in the clojure.data.xml namespace).
The docs for ann-record say
...
; a record in another namespace
(ann-record another.ns.TheirRecord
[str :- String,
vec :- (Vec Number)])
...
So in this particular case:
(t/ann-record clojure.data.xml.Element
[tag :- t/Keyword
attrs :- (t/HMap :complete? false)
content :- (t/Vec Element)])