duct
duct copied to clipboard
POST body: how to translate camel case JSON to Clojure kebab?
Hi,
I am implementing a little REST API with Duct / Integrant, following Duct's Guide and this tutorial.
POST body JSON described there is case agnostic, so it's not clear to me how to translate a typical camel case JSON into typical kebab case Clojure maps.
Thanks in advance !
Luis https://promesante.github.io/ https://github.com/promesante
Duct uses Ring and the Muuntaja middleware to handle JSON, so you can do this in several different ways. You probably want to start with a key conversion library like camel-snake-kebab, and then either insert your own middleware, or more elegantly, update the options on the Muuntaja middleware.
The Muuntaja middleware handles content negotiation, and is configured by the :duct.middleware.web/format
key, which is added automatically by the :duct.module.web/api
module. You can just override that key in your configuration and add the options you want. It looks like there might be a :decoder-key-fn
option in Muuntaja, but you'd need to investigate that.
hi James,
I am trying to "update the options on the Muuntaja middleware", as suggested.
In REPL, everything seems to work fine till invocation to function deep-merge
, inside :duct.middleware.web/format
.
> (require '[muuntaja.core :as mc])
> (require '[camel-snake-kebab.core :as csk])
> mc/default-options
...
:formats
{"application/json"
{:name "application/json",
:encoder [#function[muuntaja.format.json/encoder]],
:decoder
[#function[muuntaja.format.json/decoder] {:decode-key-fn true}],
:return nil,
:matches nil},
...
> (defn deep-merge [a b]
(if (and (map? a) (map? b))
(merge-with deep-merge a b)
b))
> (def formats (let [current-decoder (get-in mc/default-options [:formats "application/json" :decoder])
new-decoder (assoc-in current-decoder [1] {:decode-key-fn #(keyword (csk/->kebab-case %))})
current-encoder (get-in mc/default-options [:formats "application/json" :encoder])
new-encoder (conj current-encoder {:encode-key-fn #(name (csk/->camelCase %))})]
{:formats {"application/json" {:decoder new-decoder :encoder new-encoder}}}))
> (deep-merge mc/default-options formats)
...
:formats
{"application/json"
{:name "application/json",
:encoder
[#function[muuntaja.format.json/encoder]
{:encode-key-fn #function[dev/fn--16866/fn--16869]}],
:decoder
[#function[muuntaja.format.json/decoder]
{:decode-key-fn #function[dev/fn--16866/fn--16867]}],
:return nil,
:matches nil},
...
I try to implement that using integrant the following way:
In config.edn
:
:duct.module.web/api {}
:authorizer.serializations/formats {}
:duct.middleware.web/format {:formats #ig/ref :authorizer.serializations/formats}
Implementation:
(ns authorizer.serializations
(:require [clojure.data.json :as json]
[integrant.core :as ig]
[muuntaja.core :as mc]
[camel-snake-kebab.core :as csk]))
(defmethod ig/init-key ::formats [_ _]
(let [current-decoder (get-in mc/default-options [:formats "application/json" :decoder])
new-decoder (assoc-in current-decoder [1] {:decode-key-fn #(keyword (csk/->kebab-case %))})
current-encoder (get-in mc/default-options [:formats "application/json" :encoder])
new-encoder (conj current-encoder {:encode-key-fn #(name (csk/->camelCase %))})]
{"application/json" {:decoder new-decoder :encoder new-encoder}}))
However, when running (go)
in integrant.repl
, I get:
dev> (go)
Execution error (IllegalArgumentException) at duct.core/expand-ancestor-keys (core.clj:69).
No implementation of method: :kv-reduce of protocol: #'clojure.core.protocols/IKVReduce found for class: muuntaja.middleware$wrap_format$fn__12129
Error fully reported:
Show: Project-Only All
Hide: Clojure Java REPL Tooling Duplicates (10 frames hidden)
1. Unhandled java.lang.IllegalArgumentException
No implementation of method: :kv-reduce of protocol:
#'clojure.core.protocols/IKVReduce found for class:
muuntaja.middleware$wrap_format$fn__12129
core_deftype.clj: 583 clojure.core/-cache-protocol-fn
protocols.clj: 175 clojure.core.protocols/fn/G
core.clj: 6856 clojure.core/reduce-kv
core.clj: 6847 clojure.core/reduce-kv
core.clj: 69 duct.core/expand-ancestor-keys
core.clj: 68 duct.core/expand-ancestor-keys
core.clj: 79 duct.core/merge-configs*
core.clj: 77 duct.core/merge-configs*
ArraySeq.java: 111 clojure.lang.ArraySeq/reduce
core.clj: 6827 clojure.core/reduce
core.clj: 6810 clojure.core/reduce
core.clj: 86 duct.core/merge-configs
core.clj: 81 duct.core/merge-configs
RestFn.java: 421 clojure.lang.RestFn/invoke
core.clj: 254 duct.core/eval8839/fn/fn
core.clj: 145 duct.core/fold-modules/fn
core.cljc: 280 integrant.core$fold$fn__8331/invoke
ArraySeq.java: 116 clojure.lang.ArraySeq/reduce
core.clj: 6827 clojure.core/reduce
core.clj: 6810 clojure.core/reduce
core.cljc: 280 integrant.core$fold/invokeStatic
core.cljc: 272 integrant.core$fold/invoke
core.clj: 145 duct.core/fold-modules
core.clj: 139 duct.core/fold-modules
core.clj: 182 duct.core/build-config
core.clj: 173 duct.core/build-config
core.clj: 193 duct.core/prep-config
core.clj: 184 duct.core/prep-config
dev.clj: 30 dev/eval10170/fn
repl.clj: 16 integrant.repl/prep/fn
AFn.java: 154 clojure.lang.AFn/applyToHelper
AFn.java: 144 clojure.lang.AFn/applyTo
Var.java: 308 clojure.lang.Var/alterRoot
core.clj: 5510 clojure.core/alter-var-root
core.clj: 5505 clojure.core/alter-var-root
RestFn.java: 425 clojure.lang.RestFn/invoke
repl.clj: 16 integrant.repl/prep
repl.clj: 14 integrant.repl/prep
repl.clj: 54 integrant.repl/go
repl.clj: 53 integrant.repl/go
REPL: 251 dev/eval16874
REPL: 251 dev/eval16874
Compiler.java: 7176 clojure.lang.Compiler/eval
Compiler.java: 7131 clojure.lang.Compiler/eval
core.clj: 3214 clojure.core/eval
core.clj: 3210 clojure.core/eval
main.clj: 414 clojure.main/repl/read-eval-print/fn
main.clj: 414 clojure.main/repl/read-eval-print
main.clj: 435 clojure.main/repl/fn
main.clj: 435 clojure.main/repl
main.clj: 345 clojure.main/repl
RestFn.java: 1523 clojure.lang.RestFn/invoke
interruptible_eval.clj: 79 nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj: 55 nrepl.middleware.interruptible-eval/evaluate
interruptible_eval.clj: 142 nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
AFn.java: 22 clojure.lang.AFn/run
session.clj: 171 nrepl.middleware.session/session-exec/main-loop/fn
session.clj: 170 nrepl.middleware.session/session-exec/main-loop
AFn.java: 22 clojure.lang.AFn/run
Thread.java: 748 java.lang.Thread/run
What am I making wrong?
Thanks in advance...
Luis https://promesante.github.io/ https://github.com/promesante
You have it almost right; you just need to put your keys into a profile:
:duct.module.web/api {}
:duct.profile/base
{...
:authorizer.serializations/formats {}
:duct.middleware.web/format {:formats #ig/ref :authorizer.serializations/formats}}
The outer configuration is for modules and profiles (which are currently a type of module). Non-module keys need to be put into a profile.
The next version of Duct will change the design a little to make the distinction between component keys and module keys more obvious.
It worked !
Thank you very much, James, for the quick, right replies !
Luis https://promesante.github.io/ https://github.com/promesante