"Idiomatic" is always a hard word to define, and I think some of the points
made here are good, but let me also provide a few guidelines I try to abide
by when writing an API:
Start with data, preferably hash maps. At some point your API will be
consumed by someone else's program. Macros make it hard to compose api
calls in a sane matter using code. So stick with hash maps and pure data.
Something like the following:
{:host "foo.bar.com"
:port 80
:path "/some/path/i/want"
:params {:name :value :key :value2}}
Now if it comes time to modify/process/compose this request we can use
normal Clojure functions like assoc/conj to build this request map. Of
course, using this approach normally results in a explosion of data, so
pretty it up with helper functions:
(make-request-map "http://foo.bar.com/some/path/i/want" {:name :value})
The key here, is that these helper functions should emit the data you
specified in the first step.
And finally, write macros as a last resort to pretty up the user experience
even further. In short:
1) Start with data to allow clojure code to easily access your API
2) Make generating that data simpler by writing helper functions to
generate data
3) (Optionally) Write a DSL to make user interaction easier.
Timothy
On Sat, Mar 12, 2016 at 6:41 AM, Johan Haleby <[email protected]>
wrote:
> Thanks a lot for your support and insights. I'm going to rewrite it to use
> "with-open" as we speak.
>
> On Sat, Mar 12, 2016 at 2:37 PM, Marc Limotte <[email protected]> wrote:
>
>> Look at the source for the clojure.core with-open macro. In the repl:
>> `(source with-open)`.
>>
>> I think Gary is right. with-open does exactly what you need, I should
>> have thought of that, and you should probably use it. But if you want to
>> get your version working, trying to understand what the with-open macro is
>> doing. Your implementation can be simpler because you only have one
>> explicit binding. Essentially you'll create a let as a backquoted form and
>> then splice in the explicit symbol from the user:
>>
>>
>> `(let [~sym ...server-instance-or-uri...] ... )
>>
>>
>> marc
>>
>>
>>
>>
>> On Sat, Mar 12, 2016 at 1:57 AM, Johan Haleby <[email protected]>
>> wrote:
>>
>>>
>>>
>>> On Wed, Mar 9, 2016 at 7:32 PM, Marc Limotte <[email protected]>
>>> wrote:
>>>
>>>> With the macro approach, they don't need to escape it.
>>>>
>>>
>>> Do you know of any resources of where I can read up on this? I have the
>>> macro working with an implicit "uri" generated but I don't know how to make
>>> it explicit (i.e. defined by the user) the way you proposed.
>>>
>>>
>>>>
>>>> On Wed, Mar 9, 2016 at 12:52 PM, Johan Haleby <[email protected]>
>>>> wrote:
>>>>
>>>>> Thanks a lot for your support Marc, really appreciated.
>>>>>
>>>>> On Wed, Mar 9, 2016 at 5:33 PM, Marc Limotte <[email protected]>
>>>>> wrote:
>>>>>
>>>>>> Yes, I was assuming the HTTP calls happen inside the
>>>>>> with-fake-routes! block.
>>>>>> I missed the part about the random port. I se 3 options for that:
>>>>>>
>>>>>> *Assign a port, rather than random*
>>>>>>
>>>>>> (with-fake-routes! 9999 ...)
>>>>>>
>>>>>>
>>>>>> But then, of course, you have to worry about port already in use.
>>>>>>
>>>>>> *An atom*
>>>>>>
>>>>>> (def the-uri (atom nil))
>>>>>> (with-fake-routes! the-uri
>>>>>> ...
>>>>>> (http/get @the-uri "/x"))
>>>>>>
>>>>>> *A macro*
>>>>>>
>>>>>> A common convention in Clojure would be to pass it a symbol (e.g.
>>>>>> `uri` that is bound by the macro), rather implicitly creating `uri`.
>>>>>>
>>>>>> (with-fake-routes! [uri option-server-instance]
>>>>>>
>>>>>> route-map
>>>>>>
>>>>>> (http/get uri "/x"))
>>>>>>
>>>>>>
>>>>> Didn't know about this convention so thanks for the tip. But is your
>>>>> snippet above actually working code or does the user need escape "uri"
>>>>> and "
>>>>> option-server-instance" using a single-quotes, i.e.
>>>>>
>>>>> (with-fake-routes! [*'*uri *'*option-server-instance] ...)
>>>>>
>>>>>
>>>>>>
>>>>>> or, with a pre-defined server
>>>>>>
>>>>>> (def fake-server ...)
>>>>>> (with-fake-routes!
>>>>>>
>>>>>> route-map
>>>>>>
>>>>>> (http/get (:uri fake-server) "/x"))
>>>>>>
>>>>>>
>>>>>> marc
>>>>>>
>>>>>>
>>>>>>
>>>>>> On Wed, Mar 9, 2016 at 1:00 AM, Johan Haleby <[email protected]>
>>>>>> wrote:
>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> On Wed, Mar 9, 2016 at 6:20 AM, Johan Haleby <[email protected]
>>>>>>> > wrote:
>>>>>>>
>>>>>>>> Thanks for your feedback, exactly what I wanted.
>>>>>>>>
>>>>>>>> On Tuesday, March 8, 2016 at 3:16:02 PM UTC+1, mlimotte wrote:
>>>>>>>>>
>>>>>>>>> I don't think you need a macro here. In any case, I'd avoid using
>>>>>>>>> a macro as late as possible. See how far you get with just
>>>>>>>>> functions, and
>>>>>>>>> then maybe at the end, add one macro if you absolutely need it to add
>>>>>>>>> just
>>>>>>>>> a touch of syntactic sugar.
>>>>>>>>>
>>>>>>>>> routes should clearly be some sort of data-structure, rather than
>>>>>>>>> side-effect setter functions. Maybe this:
>>>>>>>>>
>>>>>>>>> (with-fake-routes!
>>>>>>>>> optional-server-instance
>>>>>>>>> route-map)
>>>>>>>>>
>>>>>>>>>
>>>>>>> Hmm now that I come to think of it I don't see how this would
>>>>>>> actually work unless you also perform the HTTP request from inside the
>>>>>>> scope of with-fake-routes!, otherwise the server instance would be
>>>>>>> closed before you get the chance to make the request. Since you
>>>>>>> make an actual HTTP request you need access to the URI generated when
>>>>>>> starting the fake-server instance (at least if the port is chosen
>>>>>>> randomly). So either I suppose you would have to do like this
>>>>>>> (which requires a macro?):
>>>>>>>
>>>>>>> (with-fake-routes!
>>>>>>> {"/x" {:status 200 :content-type "application/json" :body (slurp
>>>>>>> (io/resource "my.json"))}}
>>>>>>> ; Actual HTTP request
>>>>>>> (http/get uri "/x"))
>>>>>>>
>>>>>>> where "uri" is created by the with-fake-routes! macro *or* we
>>>>>>> could return the generated fake-server. But if so with-fake-routes!
>>>>>>> cannot
>>>>>>> automatically close the fake-server instance since we need the
>>>>>>> instance to be alive when we make the call to the generated uri. I
>>>>>>> suppose
>>>>>>> it would have to look something like this:
>>>>>>>
>>>>>>> (let [fake-server (with-fake-routes! {"/x" {:status 200
>>>>>>> :content-type "application/json" :body (slurp (io/resource
>>>>>>> "my.json"))}})]
>>>>>>> (http/get (:uri fake-server) "/x")
>>>>>>> (shutdown! fake-server))
>>>>>>>
>>>>>>> If so I think that the second option is unnecessary since then you
>>>>>>> might just go with:
>>>>>>>
>>>>>>> (with-fake-routes!
>>>>>>> *required*-server-instance
>>>>>>> route-map)
>>>>>>>
>>>>>>> instead of having two options. But then we loose the niceness of
>>>>>>> having the server instance be automatically created and stopped for us?
>>>>>>>
>>>>>>>
>>>>>>>>> Where optional-server-instance, if it exists is, an object
>>>>>>>>> returned by (fake-server/start!). If optional-server-instance is
>>>>>>>>> not passed in, then with-fake-routes! creates it's own and is
>>>>>>>>> free to call (shutdown!) on it automatically. And route-map is a
>>>>>>>>> Map of routes:
>>>>>>>>>
>>>>>>>>
>>>>>>>>> {
>>>>>>>>> "/x"
>>>>>>>>> {:status 200 :content-type "application/json" :body (slurp
>>>>>>>>> (io/resource "my.json"))}
>>>>>>>>> {:path "/y" :query {:q "something")}}
>>>>>>>>> {:status 200 :content-type "application/json" :body (slurp
>>>>>>>>> (io/resource "my2.json"))}
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>>
>>>>>>>> +1. I'm gonna go for this option.
>>>>>>>>
>>>>>>>>
>>>>>>>>>
>>>>>>>>> Also, at the risk of scope creep, I could foresee wanting the
>>>>>>>>> response to be based on the input instead of just a static blob. So
>>>>>>>>> maybe
>>>>>>>>> the value of :body could be a string or a function of 1 arg, the
>>>>>>>>> route-- in
>>>>>>>>> your code test with (fn?).
>>>>>>>>>
>>>>>>>>
>>>>>>>> That's a good idea indeed. I've already thought about this for
>>>>>>>> matching the request. I'd like this to work:
>>>>>>>>
>>>>>>>> {
>>>>>>>> (fn [request] (= (:path request) "/x"))
>>>>>>>> {:status 200 :content-type "application/json" :body (slurp
>>>>>>>> (io/resource "my.json"))}
>>>>>>>> {:path "/y" :query {:q (fn [q] (clojure.string/starts-with? q
>>>>>>>> "some"))}}
>>>>>>>> {:status 200 :content-type "application/json" :body (slurp
>>>>>>>> (io/resource "my2.json"))}
>>>>>>>> }
>>>>>>>>
>>>>>>>> Thanks a lot for your help and feedback!
>>>>>>>>
>>>>>>>>
>>>>>>>>>
>>>>>>>>> This gives you a single api, no macros, optional auto-server
>>>>>>>>> start/stop or explicit server management.
>>>>>>>>>
>>>>>>>>> marc
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Tue, Mar 8, 2016 at 3:10 AM, Johan Haleby <[email protected]>
>>>>>>>>> wrote:
>>>>>>>>>
>>>>>>>>>> Hi,
>>>>>>>>>>
>>>>>>>>>> I've just committed an embryo of an open source project
>>>>>>>>>> <https://github.com/johanhaleby/fake-http> to fake http requests
>>>>>>>>>> by starting an actual (programmable) HTTP server. Currently the API
>>>>>>>>>> looks
>>>>>>>>>> like this (which in my eyes doesn't look very Clojure idiomatic):
>>>>>>>>>>
>>>>>>>>>> (let [fake-server (fake-server/start!)
>>>>>>>>>> (fake-route! fake-server "/x" {:status 200 :content-type
>>>>>>>>>> "application/json" :body (slurp (io/resource "my.json"))})
>>>>>>>>>> (fake-route! fake-server {:path "/y" :query {:q
>>>>>>>>>> "something")}} {:status 200 :content-type "application/json" :body
>>>>>>>>>> (slurp (io/resource "my2.json"))})]
>>>>>>>>>> ; Do actual HTTP request
>>>>>>>>>> (shutdown! fake-server))
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> fake-server/start! starts the HTTP server on a free port (and
>>>>>>>>>> thus have side-effects) then you add routes to it by using
>>>>>>>>>> fake-route!. The first route just returns an HTTP response with
>>>>>>>>>> status code 200 and content-type "application/json" and the specified
>>>>>>>>>> response body if a request is made with path "/x". The second line
>>>>>>>>>> also
>>>>>>>>>> matches that a query parameter called "q" must be equal to
>>>>>>>>>> "something. In
>>>>>>>>>> the end the server is stopped.
>>>>>>>>>>
>>>>>>>>>> I'm thinking of converting all of this into a macro that is used
>>>>>>>>>> like this:
>>>>>>>>>>
>>>>>>>>>> (with-fake-routes!
>>>>>>>>>> "/x" {:status 200 :content-type "application/json" :body (slurp
>>>>>>>>>> (io/resource "my.json"))}
>>>>>>>>>> {:path "/y" :query {:q "something")}} {:status 200 :content-type
>>>>>>>>>> "application/json" :body (slurp (io/resource "my2.json"))})
>>>>>>>>>>
>>>>>>>>>> This looks better imho and it can automatically shutdown the
>>>>>>>>>> webserver afterwards but there are some potential problems. First of
>>>>>>>>>> all,
>>>>>>>>>> since starting a webserver is (relatively) slow it you might want to
>>>>>>>>>> do
>>>>>>>>>> this once for a number of tests. I'm thinking that perhaps as an
>>>>>>>>>> alternative (both options could be available) it could be possible
>>>>>>>>>> to first
>>>>>>>>>> start the fake-server and then supply it to with-fake-routes! as
>>>>>>>>>> an additional parameter. Something like this:
>>>>>>>>>>
>>>>>>>>>> (with-fake-routes!
>>>>>>>>>> fake-server ; We pass the fake-server as the first
>>>>>>>>>> argument in order to have multiple tests sharing the same fake-server
>>>>>>>>>> "/x" {:status 200 :content-type "application/json" :body (slurp
>>>>>>>>>> (io/resource "my.json"))}
>>>>>>>>>> {:path "/y" :query {:q "something")}} {:status 200 :content-type
>>>>>>>>>> "application/json" :body (slurp (io/resource "my2.json"))})
>>>>>>>>>>
>>>>>>>>>> If so you would be responsible for shutting it down just as in
>>>>>>>>>> the initial example.
>>>>>>>>>>
>>>>>>>>>> Another thing that concerns me a bit with the macro is that
>>>>>>>>>> routes doesn't compose. For example you can't define the route
>>>>>>>>>> outside of
>>>>>>>>>> the with-fake-routes! body and just supply it as an argument to
>>>>>>>>>> the macro (or can you?). I.e. I think it would be quite nice to be
>>>>>>>>>> able to
>>>>>>>>>> do something like this:
>>>>>>>>>>
>>>>>>>>>> (let [routes [["/x" {:status 200 :content-type "application/json"
>>>>>>>>>> :body (slurp (io/resource "my.json"))}]
>>>>>>>>>> [{:path "/y" :query {:q "something")}} {:status 200
>>>>>>>>>> :content-type "application/json" :body (slurp (io/resource
>>>>>>>>>> "my2.json"))}]]]
>>>>>>>>>> (with-fake-routes routes))
>>>>>>>>>>
>>>>>>>>>> Would this be a good idea? Would it make sense to have overloaded
>>>>>>>>>> variants of the with-fake-routes! macro to accommodate this as
>>>>>>>>>> well? Should it be a macro in the first place? What do you think?
>>>>>>>>>>
>>>>>>>>>> Regards,
>>>>>>>>>> /Johan
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> You received this message because you are subscribed to the Google
>>>>>>>>>> Groups "Clojure" group.
>>>>>>>>>> To post to this group, send email to [email protected]
>>>>>>>>>> Note that posts from new members are moderated - please be
>>>>>>>>>> patient with your first post.
>>>>>>>>>> To unsubscribe from this group, send email to
>>>>>>>>>> [email protected]
>>>>>>>>>> For more options, visit this group at
>>>>>>>>>> http://groups.google.com/group/clojure?hl=en
>>>>>>>>>> ---
>>>>>>>>>> You received this message because you are subscribed to the
>>>>>>>>>> Google Groups "Clojure" group.
>>>>>>>>>> To unsubscribe from this group and stop receiving emails from it,
>>>>>>>>>> send an email to [email protected].
>>>>>>>>>> For more options, visit https://groups.google.com/d/optout.
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>> You received this message because you are subscribed to the Google
>>>>>>>> Groups "Clojure" group.
>>>>>>>> To post to this group, send email to [email protected]
>>>>>>>> Note that posts from new members are moderated - please be patient
>>>>>>>> with your first post.
>>>>>>>> To unsubscribe from this group, send email to
>>>>>>>> [email protected]
>>>>>>>> For more options, visit this group at
>>>>>>>> http://groups.google.com/group/clojure?hl=en
>>>>>>>> ---
>>>>>>>> You received this message because you are subscribed to a topic in
>>>>>>>> the Google Groups "Clojure" group.
>>>>>>>> To unsubscribe from this topic, visit
>>>>>>>> https://groups.google.com/d/topic/clojure/gieS5hQCUm4/unsubscribe.
>>>>>>>> To unsubscribe from this group and all its topics, send an email to
>>>>>>>> [email protected].
>>>>>>>> For more options, visit https://groups.google.com/d/optout.
>>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> You received this message because you are subscribed to the Google
>>>>>>> Groups "Clojure" group.
>>>>>>> To post to this group, send email to [email protected]
>>>>>>> Note that posts from new members are moderated - please be patient
>>>>>>> with your first post.
>>>>>>> To unsubscribe from this group, send email to
>>>>>>> [email protected]
>>>>>>> For more options, visit this group at
>>>>>>> http://groups.google.com/group/clojure?hl=en
>>>>>>> ---
>>>>>>> You received this message because you are subscribed to the Google
>>>>>>> Groups "Clojure" group.
>>>>>>> To unsubscribe from this group and stop receiving emails from it,
>>>>>>> send an email to [email protected].
>>>>>>> For more options, visit https://groups.google.com/d/optout.
>>>>>>>
>>>>>>
>>>>>> --
>>>>>> You received this message because you are subscribed to the Google
>>>>>> Groups "Clojure" group.
>>>>>> To post to this group, send email to [email protected]
>>>>>> Note that posts from new members are moderated - please be patient
>>>>>> with your first post.
>>>>>> To unsubscribe from this group, send email to
>>>>>> [email protected]
>>>>>> For more options, visit this group at
>>>>>> http://groups.google.com/group/clojure?hl=en
>>>>>> ---
>>>>>> You received this message because you are subscribed to a topic in
>>>>>> the Google Groups "Clojure" group.
>>>>>> To unsubscribe from this topic, visit
>>>>>> https://groups.google.com/d/topic/clojure/gieS5hQCUm4/unsubscribe.
>>>>>> To unsubscribe from this group and all its topics, send an email to
>>>>>> [email protected].
>>>>>> For more options, visit https://groups.google.com/d/optout.
>>>>>>
>>>>>
>>>>> --
>>>>> You received this message because you are subscribed to the Google
>>>>> Groups "Clojure" group.
>>>>> To post to this group, send email to [email protected]
>>>>> Note that posts from new members are moderated - please be patient
>>>>> with your first post.
>>>>> To unsubscribe from this group, send email to
>>>>> [email protected]
>>>>> For more options, visit this group at
>>>>> http://groups.google.com/group/clojure?hl=en
>>>>> ---
>>>>> You received this message because you are subscribed to the Google
>>>>> Groups "Clojure" group.
>>>>> To unsubscribe from this group and stop receiving emails from it, send
>>>>> an email to [email protected].
>>>>> For more options, visit https://groups.google.com/d/optout.
>>>>>
>>>>
>>>> --
>>>> You received this message because you are subscribed to the Google
>>>> Groups "Clojure" group.
>>>> To post to this group, send email to [email protected]
>>>> Note that posts from new members are moderated - please be patient with
>>>> your first post.
>>>> To unsubscribe from this group, send email to
>>>> [email protected]
>>>> For more options, visit this group at
>>>> http://groups.google.com/group/clojure?hl=en
>>>> ---
>>>> You received this message because you are subscribed to a topic in the
>>>> Google Groups "Clojure" group.
>>>> To unsubscribe from this topic, visit
>>>> https://groups.google.com/d/topic/clojure/gieS5hQCUm4/unsubscribe.
>>>> To unsubscribe from this group and all its topics, send an email to
>>>> [email protected].
>>>> For more options, visit https://groups.google.com/d/optout.
>>>>
>>>
>>> --
>>> You received this message because you are subscribed to the Google
>>> Groups "Clojure" group.
>>> To post to this group, send email to [email protected]
>>> Note that posts from new members are moderated - please be patient with
>>> your first post.
>>> To unsubscribe from this group, send email to
>>> [email protected]
>>> For more options, visit this group at
>>> http://groups.google.com/group/clojure?hl=en
>>> ---
>>> You received this message because you are subscribed to the Google
>>> Groups "Clojure" group.
>>> To unsubscribe from this group and stop receiving emails from it, send
>>> an email to [email protected].
>>> For more options, visit https://groups.google.com/d/optout.
>>>
>>
>> --
>> You received this message because you are subscribed to the Google
>> Groups "Clojure" group.
>> To post to this group, send email to [email protected]
>> Note that posts from new members are moderated - please be patient with
>> your first post.
>> To unsubscribe from this group, send email to
>> [email protected]
>> For more options, visit this group at
>> http://groups.google.com/group/clojure?hl=en
>> ---
>> You received this message because you are subscribed to a topic in the
>> Google Groups "Clojure" group.
>> To unsubscribe from this topic, visit
>> https://groups.google.com/d/topic/clojure/gieS5hQCUm4/unsubscribe.
>> To unsubscribe from this group and all its topics, send an email to
>> [email protected].
>> For more options, visit https://groups.google.com/d/optout.
>>
>
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to [email protected]
> Note that posts from new members are moderated - please be patient with
> your first post.
> To unsubscribe from this group, send email to
> [email protected]
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
> ---
> You received this message because you are subscribed to the Google Groups
> "Clojure" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to [email protected].
> For more options, visit https://groups.google.com/d/optout.
>
--
“One of the main causes of the fall of the Roman Empire was that–lacking
zero–they had no way to indicate successful termination of their C
programs.”
(Robert Firth)
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to [email protected]
Note that posts from new members are moderated - please be patient with your
first post.
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.