Fork me on GitHub
#beginners
<
2022-02-01
>
absolutejam10:02:37

Is there an up-to-date starting example for a Clojure REST API (ideally with OpenAPI capabilities)? I'm dipping my toes again, but I'm not sure if I should be using Leiningen/Clojure CLI, compojure/reitit, etc.

teodorlu10:02:36

I think I'd ask in #reitit. I found this example, but I'm not sure whether it reflects current best practices: https://github.com/metosin/reitit/blob/master/examples/ring-malli-swagger/src/example/server.clj

absolutejam10:02:42

I've just found https://www.youtube.com/watch?v=R5YZRfABbWY so I'll follow that to begin with

practicalli-johnny11:02:08

For my last employer, I built an API with Reitit and Reitit ring handlers, including OpenAPI metadata. I used Clojure CLI and Integrant/Areo for system services, mulog for logging. I will be writing this up for Practicalli Clojure Web Services over the next few weeks. Happy to share some of the code if you DM me if you need sooner

absolutejam13:02:18

I'll see how I get on, but I'm looking forward to the write-up!

Arthur14:02:42

Kelvin’s series is pretty good if you’re looking for tutorials on how to build API’s using Clojure. His yt channel also features videos on how to use aero (for config management) and buddy (for authentication) -be sure to check those :-)

dorab18:02:35

And if you're looking for a client-side OpenAPI lib, check out oliyh/martian.

absolutejam16:02:13

I'll bookmark that, but looking for server-side atm

absolutejam16:02:42

Ideally want to be able to generate a client in another language (F# is my go-to) but I need the API to be on JVM, and clojure is fun to play with

rmxm12:02:30

I am trying to use: ring.middleware.multipart-param/multipart-params-request inside my handler, need to read multipart inside stripe webhook. Sadly not getting any data:

{:reitit.core/match #reitit.core.Match{:template "/stripe/webhook", :data {:muuntaja <<Muuntaja>>, :post {:handler #function[ggod-engine.web/payment-webhook]}}, :result #reitit.ring.Methods{:get nil, :head nil, :post #reitit.ring.Endpoint{:data {:muuntaja <<Muuntaja>>, :handler #function[ggod-engine.web/payment-webhook]}, :handler #function[ggod-engine.web/payment-webhook], :path "/stripe/webhook", :method :post, :middleware []}, :put nil, :delete nil, :connect nil, :options #reitit.ring.Endpoint{:data {:muuntaja <<Muuntaja>>, :no-doc true, :handler #function[reitit.ring/fn--2201/fn--2210]}, :handler #function[reitit.ring/fn--2201/fn--2210], :path "/stripe/webhook", :method :options, :middleware []}, :trace nil, :patch nil}, :path-params {}, :path "/stripe/webhook"}, :reitit.core/router #object[reitit.core$mixed_router$reify__1681 0x268930d "reitit.core$mixed_router$reify__1681@268930d"], :ssl-client-cert nil, :protocol "HTTP/1.1", :remote-addr "127.0.0.1", :params {}, :headers {"accept" "*/*; q=0.5, application/xml", "cache-control" "no-cache", "user-agent" "Stripe/1.0 (+)", "host" "localhost:8004", "accept-encoding" "gzip", "content-length" "2278", "stripe-signature" "t=1643717058,v1=d4e32ca4eeadc7cfee0d033a5c5540935aebb8ccb397b6e53f1eeced522c04ef,v0=0c6b86cca3ddb10c507d2d4bbcb2123e64145517e8fda85444f85b251c68ced4", "content-type" "application/json; charset=UTF-8"}, :server-port 8004, :content-length 2278, :form-params {}, :query-params {}, :content-type "application/json; charset=UTF-8", :character-encoding "UTF-8", :uri "/stripe/webhook", :server-name "localhost", :query-string nil, :path-params {}, :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x361a3c6c "HttpInputOverHTTP@361a3c6c[c=0,q=0,[0]=null,s=STREAM]"], :multipart-params {}, :scheme :http, :request-method :post}
I thought its gonna parse the "HttpInputOverHTTP" body into some usable :multipart-params. I'm doing this inside handler, don't need to wrap, other endpoints dont use multipart stuff.

hiredman12:02:10

The content-type is not correct for multipart

rmxm12:02:12

Ok, so I am getting a json from stripe forwarded to my webhook. Yeah I notice the function I use checks for "multipart/form-data"... So how do I access the body? ring.util.request/body-string ??? Ok does the trick, thanks 🙂

Nom Nom Mousse13:02:57

How would you rewrite this to use the threading-macro (-> wildcards ...)

(dedupe
  (for [wc-row wildcards]
       (select-keys wc-row rule-wildcards)))
I'm trying to use fewer for-comprehensions

pyry14:02:56

(->> wildcards (map #(select-keys % rule-wildcards) dedupe)

pyry14:02:32

Nothing wrong with using for-comprehensions though, in my opinion.

pyry14:02:16

And note: I'm starting with thread-last instead of thread-first, as ->> is the best bet when operating on collections (clojure core functions typically handle a collection as the last operand of a function)

Nom Nom Mousse14:02:01

Thanks! In that case I feel my approach is better since I avoid a map function inside the thread. I wondered if something like

(-> wildcards
    apply
    (select-keys rule-wildcards)
    dedupe)
was possible.

pyry14:02:32

Not possible in this case - select-keys must be applied to each item in the collection - not at once to the whole collection -, which is what map (and your for-example) gets you.

🎯 1
Dumch15:02:02

Does this kind of code make sense as an optimization? (if (string? zip) (ZipFile. ^String zip) (ZipFile. ^File zip)) Without it warn-on-reflection whines call to java.util.zip.ZipFile ctor can't be resolved.

Ben Sless16:02:09

It's always good to avoid reflection warnings. If you want to keep it more open rather than use conditional you could make a protocol

ghadi16:02:22

@arturdumchev yes, that sort of conditional is common when calling an overloaded method

✔️ 1
Zayn Malik16:02:01

I am a beginner in clojurescript. I wanted to ask how can I use Axios in clojurescript. I have seen cljsjs libraries solve this issue but I don't know how to import it in my deps.edn. The deps there are listed as:

{:deps {org.clojure/clojure {:mvn/version "1.10.0"}
        org.clojure/clojurescript {:mvn/version "1.10.773"}
        reagent/reagent {:mvn/version "0.10.0"}}
I am unable to understand how to import cljsjs/axios using it. Any help is appreciated. Thank you.

Zayn Malik17:02:54

I have found the solution thank you

cljsjs/axios {:mvn/version "0.19.0-0"}

👍 1
DenisMc20:02:36

Hi, I have a list of maps coming from a db where I need to transform both keys and values in various ways to enable consumption through a web service. So I have a list like this:

(def val [{:col1 "1" :col2 1M}
 {:col1 "3" :col2 2M}])
And I need to transform both keys and values. I have the value transformation cracked like so:
(->> val
     (map #(medley/update-existing % [:col2] double))
     )
But I can’t figure out how to rename (for example) the key :col1 to :new-col1. No doubt this is trivial but some help would be appreciated.

ghadi20:02:11

@denis.mccarthy.kerry into is useful for this sort of thing

(into {}
      (fn [[k v]] 
        [(something k) (something-else v)])
      original)

DenisMc21:02:48

Thanks, I’m using set/rename-keys. I had the key replacement map in a vector originally which is why it wasn’t working facepalm

DenisMc21:02:25

OK, one more gnarly one that maybe you guys could help with: I also need to transform a few date values in the list. For a single map, this sort of structure works:

(-> data (mc/update-existing :my_date #(format-date % timezone)))
but if I generalise this to a list of maps then I end up with something like:
(map #(mc/update-existing :activation_date #(format-date % timezone)))
..and I have nested function literals. Any thoughts on how I might approach this problem?

dpsutton21:02:56

just don’t use nested function literals

DenisMc21:02:49

What’s the alternative?

dpsutton21:02:52

#(format-date % timezone) -> (fn [x] (format-date x timezone)) is just a mechanical change is almost exactly what the reader is doing

dpsutton21:02:25

the alternative is exactly what the suggestion was early with into: (fn [[k v]]

DenisMc21:02:27

ok, I was wondering whether there was a way that I could specify the argument positioning that the function literal allows using some other mechanism. No doubt the into suggestion will work, I’m just not that familiar with it and I could spend as long again figuring out the correct syntax for what I want to do. I’ll give it a go though!

ghadi21:02:04

most "update" style functions do not need a function literal as an argument, because they support trailing args: (update x :foo + 15) === (update x :foo (fn [x] (+ x 15)) in your case:

(mc/update-existing thing :activation_date format-date timezone)

ghadi21:02:31

update, update-in, swap! are three such functions in clojure.core

phronmophobic21:02:52

>

(map #(mc/update-existing :activation_date #(format-date % timezone)))
As a rule of thumb, I would avoid replacing keys, especially with different data types. It depends on the use case, but I would generally prefer adding a new key. Namespaced keys makes this much easier. For example:
(assoc data
       :my-model/activation-date (format-date (:activation_date)
                                              timezone))
This avoids :activation_date having different meanings in different contexts which can be a big win for readability.

DenisMc21:02:34

Thanks for the advice. I do in general, this is for a small number of keys where the names are slightly different because of different conventions in the db layer and the web/json layer (e.g. booleans prefixed by ‘is_’ in the db, while not so in our REST API).

👍 1
DenisMc22:02:33

Thanks so much for the help. It’s the map that’s throwing me. To boil it down, here’s my simplest case: Date format fn looks like this:

(defn format-date [date timezone]...
And I want to transform the :date fields in all maps in the list
(def val [{:col1 "1" :col2 1M :date (jt/instant)}
          {:col1 "3" :col2 2M :date (jt/instant)}])
Here’s what I have now:
(->> val
  (map (mc/update-existing :date format-date "UTC")))
But I’m getting find not supported on type: clojure.lang.Keyword . I’m using the thread last macro because I have a few of these transformations and chaining them together makes sense. TIA.

Rupert (All Street)22:02:11

To expand on @UEQPKG7HQ point, Maybe you wanted something like this:

(->> val
  (map #(mc/update-existing % :date format-date "UTC")))

DenisMc10:02:40

Thanks a lot, that’s working now