This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-12-19
Channels
- # admin-announcements (1)
- # adventofcode (14)
- # announcements (2)
- # asami (7)
- # babashka (9)
- # beginners (41)
- # calva (43)
- # cider (31)
- # clerk (2)
- # clojure (34)
- # clojure-europe (17)
- # clojure-nl (1)
- # clojure-norway (166)
- # clojure-uk (7)
- # clojurescript (4)
- # datomic (1)
- # fulcro (10)
- # garden (1)
- # hoplon (2)
- # humbleui (4)
- # hyperfiddle (12)
- # jobs-discuss (6)
- # quil (6)
- # ring (6)
- # shadow-cljs (55)
- # squint (8)
- # xtdb (26)
Det ble en liten bloggpost til før jula tar oss, denne gangen en liten praktisk sak om hvordan du kan gjøre ñ
om n
når du trenger det: https://parenteser.mattilsynet.io/unicode-normalisering/
Er det slik en vanligvis legger til en ny nøkkel-verdi i et map? :thinking_face:
(let [request {:headers {"Accept" "application/foo"
"Content-Type" "application/json"
"Authorization" "token my-access-token"}
:query-params {:organization "my-org"}}]
(update-in request [:query-params]
merge {:continuationToken "continuation-token"}))
Resultat:
{:headers {"Accept" "application/foo", "Content-Type" "application/json", "Authorization" "token my-access-token"},
:query-params {:organization "my-org", :continuationToken "continuation-token"}}
Nei:
(let [request {:headers {"Accept" "application/foo"
"Content-Type" "application/json"
"Authorization" "token my-access-token"}
:query-params {:organization "my-org"}}]
(assoc-in request [:query-params :continuationToken] "continuation-token"))
Du trenger forøvrig ikke på bruke update-in
når du bare har ett element i vektoren. Da kan du heller gjøre (update m :query-params f)
Ja, hvis du bare vil oppdatere verdien til en key rett i mappet, sånn som du gjorde over der
Altså:
(update-in request [:query-params] merge {:continuationToken "token")
Er det samme som:
(update request :query-params merge {:continuationToken "token"})
Aha, jeg forvekslet begrepene "legge til" og "oppdatere," men det er vel synonymer i denne konteksten ("legge til" er også en "oppdatering" av mappet en begynner med).
Flere måter å oppnå samme resultat:
(assoc-in request [:query-params :continuationToken] "token")
(update request :query-params assoc :continuationToken "token")
Jeg var ikke klar over at en kunne bruke en vektor med keywords på den måten får å "rusle nedover i maps." Det er jo veldig praktisk!
Nå gjør jo alle disse til slutt det samme, men jeg mener assoc-in
-varianten er den som tydeligst kommuniserer hensikten din
Disse er spesielt nyttige sammen med thread first:
(let [request {:headers {"Accept" "application/foo"
"Content-Type" "application/json"
"Authorization" "token my-access-token"}
:query-params {:organization "my-org"}}]
(-> request
(assoc :method :get)
(assoc-in [:query-params :continuationToken] "continuation-token")
(assoc-in [:headers :lol] "lol!")))
kan være fint å blande update og assoc noen ganger og. Eks:
(-> thing
(update :style assoc
:color "#fff"
:font-weight "bold"))
Ja, jeg prøvde å vise en blanding av update/update-in/assoc/assoc-in, men fantasien min sviktet meg 😂
Og sånn bare for ordensskyld. Jeg er av den klare oppfatning at https://en.wikipedia.org/wiki/Law_of_Demeter også gjelder for Clojure. Det betyr at hvis du enten gjør:
(update-in x [:foo :bar :baz 0 :qix]
eller begynner å synes at specter
ser bra ut, så gjør du noe feil.
Enig! Det er mange gode use cases for update-in og assoc-in med to nivåer (som eksemplifisert over), men så faller det bratt deretter
ah, fint at den tingen der har et navn. Jeg kaller det “skrive til nøsta data er et rødt flagg”
Jeg kom innpå temaet fordi jeg oppdaget at man kan bygge opp HTTP requests som rene maps på denne måten:
(ns pulumi
(:require [cheshire.core :as json]
[org.httpkit.client :as http-kit]))
(def base-uri "")
(def access-token "X")
(def headers
{"Accept" "application/foo"
"Content-Type" "application/json"
"Authorization" (str "token " access-token)})
(defn fetch-stacks [request callback]
(http-kit/request (assoc request
:url (str base-uri "/api/user/stacks")
:method :get)
callback))
(def base-request
{:query-params {:organization "my-org"}
:headers headers})
(defn parse-response [response]
(-> (:body response)
json/parse-string))
(def stacks (fetch-stacks base-request parse-response))
(get @stacks "continuationToken")
Enig i prinsippet, men jeg syns eksempelet med http requests er et godt eksempel på fin bruk av assoc-in. Det er ikke å forgripe seg å vite at inne i headers ligger det headere.
Nå prøver jeg å finne ut hvordan jeg kan legge til continuation-token
som et optional parameter til fetch-stacks
funksjonen min, sånn at jeg kan hente alle batches/"pages" fra API-et til Pulumi ved hjelp av iteration
😁
Med if
kan det bli:
(if continue?
(assoc-in request [:query-params :continuationToken] "continuation-token")
request))
Her syns jeg cond->
er et fint alternativ:
(cond-> request
continue? (assoc-in [:query-params :continuationToken] "continuation-token"))
Hvis du fra før har
(-> m
x
y)
og skal legge til en ny optional, kan det bli:
(cond-> m
true x
true y
continue? …)
det er nesten som om Rich har tenkt litt når han satte sammen disse byggeklossene til oss 😄
Jeg må si at akkurat som med veldig smarte destructurings, så sliter jeg litt med smarte threadings også…
Dumt spørsmål: I eksempelet ditt, @U0523NZUF, er continue?
en predicate-funksjon som sjekker hvorvidt en continuation token eksisterer i responsen?
Jeg laget forresten en Gist med koden min her 🙂 https://gist.github.com/leifericf/2574d715a938b9fb0e98418ba4d77f36
Siden du skal bruke iteration
, bruker du :kf
for å lese ut continuation-token fra responsen til foregående step:
(iteration step :kf (fn [response] (:continuationToken response)) ...)
eller :kf #(:continuationToken %)
iteration sender (kf ret)
(hvis ikke nil) som arg til step-funksjonen din:
(defn step [continuation-token] …)
som da kan bruke:
(cond-> request
continuation-token (assoc-in [:query-params :continuationToken] continuation-token))
På første kall er continuation-token nil
(eller :initk
, hvis du inkluderer den til iteration), deretter er den alltid non-nil (kf ret) i hvert step. Koden over legger den altså kun på fra og med step 2.
Hvis du trenger å initialisere noe inn i step-funksjonen, kan du også gjøre det slik:
(defn step [& {:keys [access-token]}]
;; setup og returner faktisk step-fn
(fn step* [continuation-token]
(let [request {:headers {"Authorization" (format "token %s" access-token)}}]
...)))
Og sette den opp slik:
(iteration (step :access-token access-token) …)
Ev. kan du også gjøre om headers og base-request fra def til defn:
(defn headers [& {:keys [access-token]}])
(defn base-request [& {:keys [access-token]}])
og bruke:
(fn step* [continuation-token]
(let [request (cond-> (base-request :access-token access-token)
continuation-token (assoc-in [:query-params :continuationToken] continuation-token))]
...))
%%%
En annen approach kunne være å droppe cond-> og alltid legge på continuation-token, og så kjøre en clean/sanitize-funksjon som fjerner nil-verdier før du sender requesten. Hvis API-et ikke skiller på blank og fraværende continuation-token, kan du faktisk alltid sende den med.
Syns her at cond-> som over er helt grei, eventuelt via:
(defn with-continuation-token [request continuation-token]
(cond-> request
continuation-token (assoc-in [:query-params :continuationToken] continuation-token)))
(let [request (with-continuation-token (base-request…) continuation-token)] …)
Og har du kommet helt hit, kan du kanskje droppe with-continuation-token, og bare ta den med i base-request:
(defn base-request [& {:keys [access-token continuation-token]}])
og gjøre cond-> der:)
(fn step* [continuation-token]
(post (base-request :access-token access-token :continuation-token continuation-token)))
..men er kanskje finest å ha continuation-token høyere opp i step* og ikke sende den ned mange nivåer.Tusen takk for tipsene! Jeg sliter litt med å henge med, må lese litt sakte og teste det ut her.
Jeg kom frem til dette:
(def stack-request
{:headers headers
:query-params {:organization pulumi-org}
:url (str pulumi-uri "/api/user/stacks")
:method :get})
(defn get-page! [continuation-token]
(let [request stack-request]
(if continuation-token
(assoc-in request [:query-params :continuationToken] continuation-token)
request)
@(http-kit/request request response-body->json)))
Men request
blir ikke oppdatert med continuation token når jeg kjører (get-page! "XYZ")
Er det ikke lov å gjøre assoc-in
på en let
-binding? :thinking_face:Du endrer ikke på request
, du lager en ny request - som du sporenstreks kaster fra deg
(defn get-page! [continuation-token]
(-> stack-request
(cond-> continuation-token
(assoc-in [:query-params :continuationToken] continuation-token))
(http-kit/request response-body->json)
deref))
Sånn, bare for å provosere @slipset litt 😄Jeg ville jo kanskje hatt den testen på continuation-token på utsiden av denne funksjonen?
mnja, det kan man si, men den mekker jo egentlig en request og så utfører den requesten.
Ja, egentlig ville jeg sende inn request
map som parameter også, men da får jeg ikke bruke get-page!
med iteration (`step` må ta ett parameter).
Det er egentlig utrolig frustrerende å vite om alle disse bitene som kan komponeres, men ikke være smart nok til å tenke på dem og sette dem sammen riktig 😂
Med en gang dere svarer, tenker jeg "så klart!" og "gudbedre så tafatt og bakstrebersk jeg er" haha!
Tror forøvrig at @U9MKYDN4Q sin cond->
ikke er helt riktig?
Hvorfor bruker man forresten deref
istedenfor @
? :thinking_face: Er det bare en smakssak?
@slipset tja, det ble seende litt rart ut uansett. godt mulig man ikke burde drive med det der med mindre det går på en linje
Jo, men det hadde nok vært mer åpenbart (for meg) hvis det hadde vært
(cond->
continuation-token (assoc-in ...))
(defn get-page! [token]
(-> stack-request
(cond-> token (assoc-in [:query-params :continuationToken] token))
(http-kit/request response-body->json)
deref))
(defn get-page! [token]
@(http-kit/request (cond-> stack-request
token (assoc-in [:query-params :continuationToken] token)) response-body->json))
Men, jeg tror min største insigelse er at jeg ville nok hatt stack-request
som param.
Jeg ville heller formattert det sånn i så fall:
(defn get-page! [token]
@(http-kit/request
(cond-> stack-request
token (assoc-in [:query-params :continuationToken] token))
response-body->json))
Mye av dette ville jo vært ungått dersom man hadde brukt et ikke-async http-client bibliotek
> Da må @U01PE7630AC slutte å ha så lange navn 😄 Yrkesskade fra C# 😂
Har en kar som puster meg i nakken, men skal bytte til et av Babashka sine innebygde ikke-async HTTP klienter etter jeg har fått det til å funke.
Det ble slik for nå:
(defn get-page!
([] (get-page! nil))
([token]
(-> stack-request
(cond-> token (assoc-in [:query-params :continuationToken] token))
(http-kit/request response-body->json)
deref)))
Da kan jeg gjøre:
(def stacks (get-page!))
Og med token:
(get-page! (get stacks "continuationToken"))
Men jeg skal få ut stack-request
til et parameter og bytte HTTP klient etter jeg har fått oppgaven i land 😄Eventuelt:
(defn get-page! [& [token]]
(-> stack-request
(cond-> token (assoc-in [:query-params :continuationToken] token))
(http-kit/request response-body->json)
deref))
Sikkert mikroskopisk dårligere ytelse, men tipper den forskjellen spises opp av nettverksaktiviteten der 🙂
& args
og destructuring er også greier jeg sliter litt med å forstå og bruke smart.
Jeg er litt var for bruk av ulike former for multi-arity fns fordi da plukker ikke clj-kondo opp at jeg gjør feil.
Er det en villet mangel i clj-kondo? Jeg har merka det sjøl, virker som om den ikke skjønner multi-arity
Jo, men hvis du har en
(defn foo [& opts]...)
så er det jo ingen mulighet å sjekke for at du har med deg noe som helst.
(defn foo [opt1 opt2 opt3]…)
sier jo at du krever disse tre opt’sene
Ja, men om du gir funksjonen to signaturer sånn @U01PE7630AC gjorde først så syns clj-kondo at alt er dritt
Kult! Nå funker iteration! Wohooo! Men den henter visst kun den første siden 😛 Så det er noe rart der.
(def all-stacks (iteration get-page!
:vf #(get % "stacks")
:kf :continuationToken))
Én response fra get-page!
ser slik ut:
{"stacks"
[{"orgName" "my-org",
"projectName" "my-project",
"stackName" "my-stack-1",
"lastUpdate" 1692103259,
"resourceCount" 14}
{"orgName" "my-org",
"projectName" "my-project",
"stackName" "my-stack-2",
"lastUpdate" 1695911748,
"resourceCount" 16}
{"orgName" "my-org",
"projectName" "my-project",
"stackName" "my-stack-3",
"lastUpdate" 1700757647,
"resourceCount" 14}
...],
"continuationToken"
"XYZ"}
Aaaah! Kanskje fordi "continuationToken"
er en string og ikke et keyword (`:continuationToken`) i responsen!
Dette funka fett!
(def all-stacks (iteration get-page!
:vf #(get % "stacks")
:kf #(get % "continuationToken")))
(defn response-body->json [response]
(-> (:body response)
(json/parse-string keyword)))
Altså, dette er bare heeelt rått.
(def all-stacks
(iteration get-page!
:vf :stacks
:kf :continuationToken))
(def stack-names
(->> all-stacks
seq
flatten
(map :stackName)
sort))
http-kit/request
returnerer en "promise." Jeg måtte konvertere den til en seq
for å gjøre mer med den.
(->> (iteration get-page! :vf :stacks :kf :contunuationToken) seq (mapcat :stackName) sort)
Nå med request map som parameter!
(defn get-page! [request & [token]]
(-> request
(cond-> token (assoc-in [:query-params :continuationToken] token))
(http-kit/request response-body->json)
deref))
(get-page! stack-request)
(iteration (partial get-page! stack-request)
:vf :stacks
:kf :continuationToken)
En liten hjelper (for mine kollegaer som ikke kan Clojure) 🙂
(defn get-all-pages! [request token-key]
(iteration (partial get-page! request)
:vf :stacks
:kf token-key))
partial funker utmerket slik du bruker den her, når du også ønsker å bruke get-page! i andre sammenhenger (hvorfor utropstegn på get?). Hvis du kun skulle bruke den som step-fn, kunne du også gjøre det slik:
(defn get-page! [request]
(fn [token]
(-> request …)))
Og så kalle den:
(iteration (get-page! stack-request) :vf …)
hvorfor utropstegn på get?Jeg trodde det var god praksis å bruke !
på funksjoner med sideeffekter, og jeg trodde get-page!
var en slik 😅
Men når jeg tenker meg om har vel mine fleste funksjoner sideeffekter fordi dette er Babashka/Shell greier.
http-kit/request
har selv ikke noe utropstegn. Utropstegnet er ofte for “destruktive” ting, eller noe sånt? :thinking_face:
Jeg bruker ! for skrive-operasjoner ja. Selv ville jeg unngått "get" for nettverks-requests, bruker typ "fetch" eller lignende i stedet
I dag har jeg oppdatert ca. 3100 Pulumi stacks via Clojure og kjørt det i produksjon i en live demo for kollegaer 😎 Det var veldig kult å kunne vise live hvordan man skape og inspisere HTTP request maps som ren data før de sendes, og kunne filtrere ut enkelte for å teste før man kjører på alt. Jeg er spesielt fornøyd med denne (men har allerede noen tanker om forbedringer):
(defn create-permission-request [team-name permission stack]
(let [codes {:read 101 :edit 102 :admin 103}
payload (-> {"addStackPermission" {"projectName" (:projectName stack)
"stackName" (:stackName stack)
"permission" (permission codes)}}
json/generate-string)]
{:headers headers
:url (format (str pulumi-uri "/orgs/%s/teams/%s")
pulumi-org team-name)
:body payload
:method :patch}))
(doall
(->> (fetch-all-pages stack-request :continuationToken)
seq
flatten
(get-stacks-by-name "dev")
(take 1)
(map #(create-permission-request "Manual-Access-Group" :read %))
(map grant-stack-permission)))
Kanskje dette er litt bedre 🙂
Bort med let
i @U9MKYDN4Q’s ånd.
(defn create-permission-request [team permission stack]
{:headers headers
:url (format (str pulumi-uri "/orgs/%s/teams/%s") pulumi-org team)
:body (-> {"addStackPermission"
{"projectName" (:projectName stack)
"stackName" (:stackName stack)
"permission" (permission {:read 101
:edit 102
:admin 103})}}
json/generate-string)
:method :patch})
Jeg prøver å få til destructuring på stack
sånn at jeg slipper å gjøre (:projectName stack)
og (:stackName stack)
🙂
det tenker jeg er ganske unødvendig - du ender opp med flere "løse" bindinger som ikke øker lesbarheten nevneverdig
men det er altså [team permission {:keys [projectName stackName]}]
i tilfelle du vil prøve
jeg er forøvrig glad i ….. eksplisitt destructuring? I mangel på et bedre navn. [team permission {project-name :projectName stack-name :stackName}]
Målet er å bli kvitt noen navn og få ting litt mer kompakt. Det der fører bare til Java-style dobbel-navning. Da ville jeg foretrukket å bruke keyword-oppslag uten destructuring.
Men enig i at det er ålreit å kunne greppe etter enkelte ting. Jeg prøver å bruke stadig mer namespaced keys, og er forsiktig med å destructure dem, med samme mål.
Ref. https://clojurians.slack.com/archives/C061XGG1W/p1703064598161949?thread_ts=1702976581.541629&cid=C061XGG1W til @slipset
Sånn! Nå har jeg endret til å bruke babashka.http-client
og babashka.json
istedenfor org.httpkit.client
og cheshire.core
. Altså, ikke-async, færre avhengigheter, mindre kode, og 100% Babashka! Og jeg fant en bug i babashka.http-client
i porteringsprosessen (er i ferd med å åpne et issue).
https://gist.github.com/leifericf/242fb222966c8cfc8beb04539aaf8fab er oppdatert med ny kode nå.
Nå også https://www.linkedin.com/feed/update/urn:li:activity:7145749161152352256/ for å promotere Clojure/Babashka litt med et virkelig use case 🙂
Oisann. Hvem er de kjekke karene her? https://www.kode24.no/artikkel/christian-og-magnar-ble-ansatt-som-duo-en-selvfolge/80676324
Jeg kom frem til dette:
(def stack-request
{:headers headers
:query-params {:organization pulumi-org}
:url (str pulumi-uri "/api/user/stacks")
:method :get})
(defn get-page! [continuation-token]
(let [request stack-request]
(if continuation-token
(assoc-in request [:query-params :continuationToken] continuation-token)
request)
@(http-kit/request request response-body->json)))
Men request
blir ikke oppdatert med continuation token når jeg kjører (get-page! "XYZ")
Er det ikke lov å gjøre assoc-in
på en let
-binding? :thinking_face:I dag har jeg oppdatert ca. 3100 Pulumi stacks via Clojure og kjørt det i produksjon i en live demo for kollegaer 😎 Det var veldig kult å kunne vise live hvordan man skape og inspisere HTTP request maps som ren data før de sendes, og kunne filtrere ut enkelte for å teste før man kjører på alt. Jeg er spesielt fornøyd med denne (men har allerede noen tanker om forbedringer):
(defn create-permission-request [team-name permission stack]
(let [codes {:read 101 :edit 102 :admin 103}
payload (-> {"addStackPermission" {"projectName" (:projectName stack)
"stackName" (:stackName stack)
"permission" (permission codes)}}
json/generate-string)]
{:headers headers
:url (format (str pulumi-uri "/orgs/%s/teams/%s")
pulumi-org team-name)
:body payload
:method :patch}))
(doall
(->> (fetch-all-pages stack-request :continuationToken)
seq
flatten
(get-stacks-by-name "dev")
(take 1)
(map #(create-permission-request "Manual-Access-Group" :read %))
(map grant-stack-permission)))
Ref. https://clojurians.slack.com/archives/C061XGG1W/p1703064598161949?thread_ts=1702976581.541629&cid=C061XGG1W til @slipset
Sånn! Nå har jeg endret til å bruke babashka.http-client
og babashka.json
istedenfor org.httpkit.client
og cheshire.core
. Altså, ikke-async, færre avhengigheter, mindre kode, og 100% Babashka! Og jeg fant en bug i babashka.http-client
i porteringsprosessen (er i ferd med å åpne et issue).
https://gist.github.com/leifericf/242fb222966c8cfc8beb04539aaf8fab er oppdatert med ny kode nå.