duct icon indicating copy to clipboard operation
duct copied to clipboard

POST body: how to translate camel case JSON to Clojure kebab?

Open promesante opened this issue 4 years ago • 4 comments

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

promesante avatar Dec 09 '19 08:12 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.

weavejester avatar Dec 09 '19 13:12 weavejester

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

promesante avatar Dec 26 '19 07:12 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.

weavejester avatar Dec 26 '19 12:12 weavejester

It worked !

Thank you very much, James, for the quick, right replies !

Luis https://promesante.github.io/ https://github.com/promesante

promesante avatar Dec 27 '19 08:12 promesante