"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 <johan.hal...@gmail.com> 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 <mslimo...@gmail.com> 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 <johan.hal...@gmail.com> >> wrote: >> >>> >>> >>> On Wed, Mar 9, 2016 at 7:32 PM, Marc Limotte <mslimo...@gmail.com> >>> 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 <johan.hal...@gmail.com> >>>> wrote: >>>> >>>>> Thanks a lot for your support Marc, really appreciated. >>>>> >>>>> On Wed, Mar 9, 2016 at 5:33 PM, Marc Limotte <mslimo...@gmail.com> >>>>> 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 <johan.hal...@gmail.com> >>>>>> wrote: >>>>>> >>>>>>> >>>>>>> >>>>>>> On Wed, Mar 9, 2016 at 6:20 AM, Johan Haleby <johan.hal...@gmail.com >>>>>>> > 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 <johan....@gmail.com> >>>>>>>>> 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 clo...@googlegroups.com >>>>>>>>>> Note that posts from new members are moderated - please be >>>>>>>>>> patient with your first post. >>>>>>>>>> To unsubscribe from this group, send email to >>>>>>>>>> clojure+u...@googlegroups.com >>>>>>>>>> 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 clojure+u...@googlegroups.com. >>>>>>>>>> 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 clojure@googlegroups.com >>>>>>>> Note that posts from new members are moderated - please be patient >>>>>>>> with your first post. >>>>>>>> To unsubscribe from this group, send email to >>>>>>>> clojure+unsubscr...@googlegroups.com >>>>>>>> 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 >>>>>>>> clojure+unsubscr...@googlegroups.com. >>>>>>>> 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 clojure@googlegroups.com >>>>>>> Note that posts from new members are moderated - please be patient >>>>>>> with your first post. >>>>>>> To unsubscribe from this group, send email to >>>>>>> clojure+unsubscr...@googlegroups.com >>>>>>> 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 clojure+unsubscr...@googlegroups.com. >>>>>>> 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 clojure@googlegroups.com >>>>>> Note that posts from new members are moderated - please be patient >>>>>> with your first post. >>>>>> To unsubscribe from this group, send email to >>>>>> clojure+unsubscr...@googlegroups.com >>>>>> 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 >>>>>> clojure+unsubscr...@googlegroups.com. >>>>>> 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 clojure@googlegroups.com >>>>> Note that posts from new members are moderated - please be patient >>>>> with your first post. >>>>> To unsubscribe from this group, send email to >>>>> clojure+unsubscr...@googlegroups.com >>>>> 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 clojure+unsubscr...@googlegroups.com. >>>>> 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 clojure@googlegroups.com >>>> Note that posts from new members are moderated - please be patient with >>>> your first post. >>>> To unsubscribe from this group, send email to >>>> clojure+unsubscr...@googlegroups.com >>>> 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 >>>> clojure+unsubscr...@googlegroups.com. >>>> 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 clojure@googlegroups.com >>> Note that posts from new members are moderated - please be patient with >>> your first post. >>> To unsubscribe from this group, send email to >>> clojure+unsubscr...@googlegroups.com >>> 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 clojure+unsubscr...@googlegroups.com. >>> 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 clojure@googlegroups.com >> Note that posts from new members are moderated - please be patient with >> your first post. >> To unsubscribe from this group, send email to >> clojure+unsubscr...@googlegroups.com >> 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 >> clojure+unsubscr...@googlegroups.com. >> 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 clojure@googlegroups.com > Note that posts from new members are moderated - please be patient with > your first post. > To unsubscribe from this group, send email to > clojure+unsubscr...@googlegroups.com > 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 clojure+unsubscr...@googlegroups.com. > 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 clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com 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 clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.