Code Monkey home page Code Monkey logo

relabel's Introduction

relabel

Build Status

An (almost) trivial declarative domain converter based on the idea of doing destructuring in reverse.

Note

As the problem this library is trying to solve (clean declarative domain conversion) appear difficult when one begin to consider all sorts of edge case (or just not-so-basic usage), one namespace is allocated for each solution approach.

Latest Version

Usage

;; In ns:
(ns my-project.core
  (:require [relabel.lazy :refer :all]))

Basic mapping:

(def college-to-fb (converter { :username (from :name)
                                :age (from :age)
                                :school (from :college) }))

(college-to-fb { :name "Peter" :age 24 :college "test" })
;; => { :username "Peter" :age 24 :school "test" }

Schema can be nested:

(def addressbook (converter { :name (from :username)
                              :address { :country (from :loc-country)
                                         :region (from :loc-region)
                                         :location (from :loc-rest) }}))

(addressbook { :username "Brown"
               :loc-country "Switzerland" :loc-region "Aargau" :loc-rest "test 1234" })
;; => { :name "Brown"
;;      :address { :country "Switzerland"
;;                 :region "Aargau"
;;                 :location "test 1234" }}

The modifiers serves as a form of DSL. For now we focus on providing their functional form (functions that either produce a function or transform another function) as they are more composable in this way. Macro could eventually be added once a good interface has solidified.

The from modifier extract values by key:

((from :tags) { :title "Hello world!" :tags ["test" "first post"] })
;; => ["test" "first post"]

((from :tags) { :title "Nothing" :tags nil })
;; => nil

((from :tags) { :title "Another post" })
;; => raise ExceptionInfo

To extract values nested in the source object, from also supports extraction through a Specter path:

(require '[com.rpl.specter :as spct])

(let [data { :reqId 123
             :req { :type :buy-scissor
                    :remarks [{ :msg "Hello world!" :by "Chris" }
                              { :msg "Me too" :by "Katie" }
                              { :msg "We've got too many paper here, would be nice if we can cut 'em all ;)" :by "Carol" }
                              { :msg "So that we can throw a party?" :by "Cindy" }]
                    :by "Tom" }
             :state :pending }]
  ((from [:req :remarks spct/LAST :msg]) data))
;; => "So that we can throw a party?"

from accept an optional parameter :then, which is a function to apply to after extracting value:

((from :tags :then #(clojure.string/join ", " %)) { :title "TDD in action" :tags ["TDD" "Best practice" "Experience Sharing"] })
;; => "TDD, Best practice, Experience Sharing"

It also accept an optional parameter :default, which can be used to avoid Exception if a matching value is not found, by using the value of the parameter instead:

((from :tags :default "#blogging") { :title "Another post" })
;; => "#blogging"

Exception can also be avoided by using loose mode, see Configs section for details.

The literal modifier allows you to set constant/fixed field:

(def test-convert (converter { :user-id (from :user-key)
                               :version (literal "2.3") }))

(test-convert { :user-key 4 })
;; => { :user-id 4 :version "2.3" }
(test-convert { :user-key 5 :version "1.2" })
;; => { :user-id 5 :version "2.3" }

The one-or-more modifier change a function so that it can deal with single object and a sequence in the same way:

(def test-convert (converter { :foo (from :bar) }))

((one-or-more test-convert) {:bar 3})
;; => {:foo 3}
((one-or-more test-convert) [{:bar 3} {:bar 42}])
;; => [{:foo 3} {:foo 42}]

See unit tests for these examples.

Configs

The library has a global config stored in the dynamic variable *config*, which is a map of configuration options vs value. One can either change them "permanantly" through the standard function alter-var-root, or apply change to a range of converters using the scoped version binding.

Currently there are two config options:

:automap-seq determines whether post-processing is applied flexibly to values extracted from source object's field in the from modifier. If true, then the function specified in the :then optional parameter will be changed with the one-or-more modifier before applying to extracted value.

;; Suppose *config* is { :automap-seq false }

((from :param-vals :then #(Integer/parseInt %)) { :param-vals ["25" "4" "56"] })
;; => raise Exception
(binding [*config* { :automap-seq true }]
  ((from :param-vals :then #(Integer/parseInt %)) { :param-vals ["25" "4" "56"] })
  ;; => [25 4 56]
  ((from :param-vals :then #(Integer/parseInt %)) { :param-vals "116" })
  ;; => 116
)

To allow more fine-grained control, the config can also be specified in the from modifier itself through the optional parameter :automap?. As it is more specific, it will override the global config if present.

;; Suppose *config* is { :automap-seq false }

((from :param-vals :then #(Integer/parseInt %) :automap? true)
  { :param-vals ["25" "4" "56"] })
;; => [25 4 56]

:strict controls the mode for the from modifier. It is in strict mode if :strict is true, and loose mode if false. In strict mode, from will raise an Exception if a value cannot be extracted from the object:

  • When selecting by keywords, this happens if the object has no key named by the keyword.
  • When selecting by a Specter path, this happens if there is no matching value or more than one match.

In loose mode, a default value of nil will be returned if there is no match. This default value can be overriden by the :default optional parameter. Note that if this parameter is present, loose mode will be used for that call of from even if we are in strict mode otherwise.

TODO

  • Support something like XPath for modifier from (use Specter?)
  • Support nested map for schema declaration in converter
  • Control on strict/loose setting when key cannot be mapped

Contact

This project is currently developed and maintained by @lemonteaa, which can be reached through email listed on the github account.

Contributing

Contributors welcome. Just report an issue or submit a pull request.

License

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.