diff --git a/src/com/latacora/backsaws/dynamodb.clj b/src/com/latacora/backsaws/dynamodb.clj new file mode 100644 index 0000000..25ebbba --- /dev/null +++ b/src/com/latacora/backsaws/dynamodb.clj @@ -0,0 +1,98 @@ +(ns com.latacora.backsaws.dynamodb + (:require [clojure.data.json :as json]) + (:import java.util.Base64)) + +(defmulti decode + "Given a DynamoDB map encoded data structure, returns a simplified clojure data structure." + (fn [encoded] + (-> encoded first key keyword))) + +(defmethod decode :BOOL [v] + (:BOOL v)) + +(defmethod decode :S [v] + (:S v)) + +(defmethod decode :SS [v] + (set (:SS v))) + +(defmethod decode :N [v] + (json/read-str (:N v))) + +(defmethod decode :NS [v] + (set (map json/read-str (:NS v)))) + +(defmethod decode :M [v] + (reduce-kv (fn [m k v] + (assoc m k (decode v))) + {} + (:M v))) + +(defmethod decode :L [v] + (mapv decode (:L v))) + +(defmethod decode :NULL [v] + nil) + +(defmethod decode :B [v] + (.decode (Base64/getDecoder) + (:B v))) + +(defn encode-bytes [^bytes input] + (new String (.encode (Base64/getEncoder) input) "utf-8")) + +(defprotocol IDynamoDBEncode + (encode [this])) + +(extend-protocol IDynamoDBEncode + (Class/forName "[B") + (encode [this] + {:B (encode-bytes this)}) + + Number + (encode [this] {:N (json/json-str this)}) + + String + (encode [this] {:S this}) + + Boolean + (encode [this] {:BOOL (if this true false)}) + + clojure.lang.IPersistentVector + (encode [this] + {:L (mapv encode this)}) + + clojure.lang.IPersistentList + (encode [this] + {:L (mapv encode this)}) + + clojure.lang.IPersistentSet + (encode [this] + (cond + (every? number? this) + {:NS (mapv json/json-str this)} + + (every? string? this) + {:SS (mapv identity this)} + + (every? bytes? this) + {:BS (mapv encode-bytes this)} + + :else + (throw + (ex-info + "Cannot encode set, is invalid.")))) + + clojure.lang.IPersistentMap + (encode [this] + {:M (reduce-kv (fn [m k v] + (assoc m k (encode v))) + {} + this)})) + +(comment + (decode {:M {:foo {:S "asd"} :bar {:N "333"} :baz {:L [{:S "asd"}]}}}) + + (decode {:N "2"}) + + (decode {:M (:Item result)})) diff --git a/test/com/latacora/backsaws/dynamodb_test.clj b/test/com/latacora/backsaws/dynamodb_test.clj new file mode 100644 index 0000000..b488279 --- /dev/null +++ b/test/com/latacora/backsaws/dynamodb_test.clj @@ -0,0 +1,52 @@ +(ns com.latacora.backsaws.dynamodb-test + (:require [com.latacora.backsaws.dynamodb :as ddb] + [clojure.test :as t])) + + +(def test-val + {:string "asd" + :number 333 + :stringset #{"this" "is" "a" "string" "set"} + :list [1 "string" 3] + :numberset #{1 2 3 4} + :bytes (.getBytes "foo") + :bool-true true + :bool-false false}) + +;; hand encoded to spec +(def encoded-val + {:M {:string {:S "asd"} + :number {:N "333"} + :stringset {:SS ["this" "is" "a" "string" "set"]} + :list {:L [{:N "1"} {:S "string"} {:N "3"}]} + :numberset {:NS ["1" "2" "3" "4"]} + :bytes {:B "Zm9v"} + :bool-true {:BOOL true} + :bool-false {:BOOL false}}}) + +(t/deftest test-decode + (t/is (= #{1 2 3} (ddb/decode {:NS ["1" "2" "3"]}))) + (t/is (= "foo" (ddb/decode {:S "foo"}))) + (t/is (= 3 (ddb/decode {:N "3"}))) + (t/is (= 3.33 (ddb/decode {:N "3.33"}))) + (t/is (= #{"foo" "bar"} (ddb/decode {:SS ["foo" "bar"]}))) + (let [eb {:B "Zm9v"}] + (t/is (= eb (ddb/encode (ddb/decode eb))))) + (t/is + (= (dissoc test-val :bytes) + (dissoc (ddb/decode encoded-val) :bytes)) + "Encode and decode are identical, except bytes, for which equality doesn't work")) + +(t/deftest test-encode + (t/testing "booleans" + (t/is (= {:BOOL true} (ddb/encode true))) + (t/is (= {:BOOL false} (ddb/encode false)))) + (t/testing "lists" + (t/is (= {:L [{:N "1"} {:N "2"} {:N "3"}]} (ddb/encode [1 2 3]))) + (t/is (= {:L [{:N "1"} {:S "string"} {:N "3"}]} (ddb/encode [1 "string" 3])))) + (t/testing "map" + (t/is (= {:M {:bar {:S "baz"} :foo {:N "2"} :nested {:M {:another {:S "value"}}}}} + (ddb/encode {:bar "baz" :foo 2 :nested {:another "value"}})))) + (t/testing "sets" + (t/is (= {:NS ["1"]} (ddb/encode #{1}))) + (t/is (= {:SS ["foo"]} (ddb/encode #{"foo"})))))