Skip to content

Commit

Permalink
Recursive beans (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfikes committed Jun 29, 2019
1 parent 5f70287 commit a036dad
Show file tree
Hide file tree
Showing 10 changed files with 1,508 additions and 153 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This change

## [Unreleased]
### Added
- Recursive beans ([#46](https://github.com/mfikes/cljs-bean/issues/46))
- Support for `IIterable` ([#32](https://github.com/mfikes/cljs-bean/issues/32))

### Changed
Expand Down
105 changes: 17 additions & 88 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,107 +4,36 @@ Like `clojure.core/bean`, but for ClojureScript.

[![Clojars Project](https://img.shields.io/clojars/v/cljs-bean.svg)](https://clojars.org/cljs-bean) [![cljdoc badge](https://cljdoc.org/badge/cljs-bean/cljs-bean)](https://cljdoc.org/d/cljs-bean/cljs-bean/CURRENT) [![Build Status](https://travis-ci.org/mfikes/cljs-bean.svg?branch=master)](https://travis-ci.org/mfikes/cljs-bean)

The `bean` function produces a thin wrapper over JavaScript objects, implementing the map abstraction:
A `bean` function and `->clj` and `->js` converters enable working with JavaScript objects via thin wrappers implementing ClojureScript collection abstractions:

```clojure
(require '[cljs-bean.core :refer [bean]])
(require '[cljs-bean.core :refer [bean ->clj ->js]])

(bean #js {:a 1, :b 2})
;; => {:a 1, :b 2}
;; {:a 1, :b 2}
```

This lets you interoperate with JavaScript objects in an idiomatic fashion, while being an order of
magnitude faster than equivalent constructs using `js->clj`:
The converters enable idiomatic interop while being much faster than
equivalent constructs using `js->clj` and `clj->js`:

```clojure
(let [{:keys [a b]} (bean #js {:a 1, :b 2})]
(let [{:keys [a b]} (->clj #js {:a 1, :b 2})]
(+ a b))
```

If a bean is going to be retained, the object passed
should be effectively immutable, as the resulting bean is backed by the object.

The `bean` function behaves like Clojure’s in that it is not recursive:

```clojure
(bean #js {:a 1, :obj #js {:x 13, :y 17}})
;; => {:a 1, :obj #js {:x 13, :y 17}}
```

## Object Extraction

Where possible, operations such as `assoc` and `conj` on a bean produce a new bean.

In these cases, the `bean?` predicate will be satisfied on the result. If so, `object`
can be used to extract the wrapped JavaScript object from the bean:

```clojure
(require '[cljs-bean.core :refer [bean bean? object]])

(assoc (bean #js {:a 1}) :b 2)
;; => {:a 1, :b 2}

(bean? *1)
;; => true

(object *2)
;; => #js {:a 1, :b 2}
```

This enables flexible and efficient ways to create JavaScript objects using Clojure idioms, without having to reach for `clj->js`.
;; => 3

(into (->clj #js [1 2]) [3 4 5])
;; [1 2 3 4 5]

For example, the following builds a JavaScript object, setting its property values:

```clojure
(let [m {:a 1, :b 2, :c 3, :d 4, :e 5, :f 6, :g 7, :h 8}]
(object (into (bean) (filter (comp odd? val)) m)))
;; => #js {:a 1, :c 3, :e 5, :g 7}
```

The example above is particularly efficient because no intermediate sequence is
generated and—owing to transients support in beans—the properties are set by
mutating a single object instance.

It is not possible for `assoc` or `conj` to produce a bean if, for example, a string key is
added to a bean configured to keywordize keys:

```clojure
(assoc (bean #js {:a 1}) "b" 2 :c 3)
;; => {:a 1, "b" 2, :c 3}

(bean? *1)
;; => false
(->js *1)
;; #js [1 2 3 4 5]
```

## Key Mapping
Read more:

By default, the map produced by `bean` keywordizes the keys. If instead you pass `:keywordize-keys` `false`,
string keys will be produced:
[Overview](doc/overview.md)

```clojure
(bean #js {:a 1, :b 2, "c/d" 3, "e f" 4} :keywordize-keys false)
;; => {"a" 1, "b" 2, "c/d" 3, "e f" 4}
```

In either of these modes, `bean` is meant to interoperate with JavaScript objects
via property names that will not be renamed by Google Closure Compiler.

You can control the key to property name mapping by supplying both `:key->prop` and `:prop->key`.
[Object Extraction](doc/object.md)

The following example mimics the behavior of ClojureScript's JavaScript object literal syntax, where
keywords are used only if properties can be represented as simple keywords:
[Recursive Beans](doc/recursive.md)

```clojure
(defn prop->key [prop]
(cond-> prop
(some? (re-matches #"[A-Za-z_\*\+\?!\-'][\w\*\+\?!\-']*" prop)) keyword))

(defn key->prop [key]
(cond
(simple-keyword? key) (name key)
(and (string? key) (string? (prop->key key))) key
:else nil))

(bean #js {:a 1, :b 2, "c/d" 3, "e f" 4} :prop->key prop->key :key->prop key->prop)
;; => {:a 1, :b 2, "c/d" 3, "e f" 4}
```
[Key Mapping](doc/key-mapping.md)
6 changes: 4 additions & 2 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
:deps {org.clojure/clojurescript {:mvn/version "1.10.520"}}
:aliases
{:repl/node {:extra-paths ["test"]
:main-opts ["-m" "cljs.main" "-re" "node"]}
:test {:extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0"}}
:extra-deps {org.clojure/test.check {:mvn/version "0.10.0-alpha4"}}
:main-opts ["-m" "cljs.main" "-re" "node"]}
:test {:extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0"}
org.clojure/test.check {:mvn/version "0.10.0-alpha4"}}
:extra-paths ["test" "cljs-test-runner-out/gen"]
:main-opts ["-m" "cljs-test-runner.main"]}}}
6 changes: 6 additions & 0 deletions doc/cljdoc.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{:cljdoc.doc/tree [["Readme" {:file "README.md"}]
["Changelog" {:file "CHANGELOG.md"}]
["Overview" {:file "doc/overview.md"}]
["Object Extraction" {:file "doc/object.md"}]
["Recursive Beans" {:file "doc/recursive.md"}]
["Key Mapping" {:file "doc/key-mapping.md"}]}
32 changes: 32 additions & 0 deletions doc/key-mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Key Mapping

By default, the map produced by `bean` keywordizes the keys. If instead you pass `:keywordize-keys` `false`,
string keys will be produced:

```clojure
(bean #js {:a 1, :b 2, "c/d" 3, "e f" 4} :keywordize-keys false)
;; => {"a" 1, "b" 2, "c/d" 3, "e f" 4}
```

In either of these modes, `bean` is meant to interoperate with JavaScript objects
via property names that will not be renamed by Google Closure Compiler.

You can control the key to property name mapping by supplying both `:key->prop` and `:prop->key`.

The following example mimics the behavior of ClojureScript's JavaScript object literal syntax, where
keywords are used only if properties can be represented as simple keywords:

```clojure
(defn prop->key [prop]
(cond-> prop
(some? (re-matches #"[A-Za-z_\*\+\?!\-'][\w\*\+\?!\-']*" prop)) keyword))

(defn key->prop [key]
(cond
(simple-keyword? key) (name key)
(and (string? key) (string? (prop->key key))) key
:else nil))

(bean #js {:a 1, :b 2, "c/d" 3, "e f" 4} :prop->key prop->key :key->prop key->prop)
;; => {:a 1, :b 2, "c/d" 3, "e f" 4}
```
64 changes: 64 additions & 0 deletions doc/object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Object Extraction

Where possible, operations such as `assoc` and `conj` on a bean produce a new bean.

In these cases, the `bean?` predicate will be satisfied on the result. If so, `object`
can be used to extract the wrapped JavaScript object from the bean:

```clojure
(require '[cljs-bean.core :refer [bean bean? object]])

(assoc (bean #js {:a 1}) :b 2)
;; => {:a 1, :b 2}

(bean? *1)
;; => true

(object *2)
;; => #js {:a 1, :b 2}
```

This enables flexible and efficient ways to create JavaScript objects
using Clojure idioms, without having to reach for `clj->js`.

For example, the following builds a JavaScript object, setting its property values:

```clojure
(let [m {:a 1, :b 2, :c 3, :d 4, :e 5, :f 6, :g 7, :h 8}]
(object (into (bean) (filter (comp odd? val)) m)))
;; => #js {:a 1, :c 3, :e 5, :g 7}
```

The example above is particularly efficient because no intermediate sequence is
generated and—owing to transients support in beans—the properties are set by
mutating a single object instance.

It is not possible for `assoc` or `conj` to produce a bean if, for example, a string key is
added to a bean configured to keywordize keys:

```clojure
(assoc (bean #js {:a 1}) "b" 2 :c 3)
;; => {:a 1, "b" 2, :c 3}

(bean? *1)
;; => false
```

The `->js` converter will automatically check and employ the fast-path
constant time conversion where possible, falling back to `clj->js` if not.

Since `->clj` and `->js` are recursive, they can be used as simplified
drop-in replacements for `js->clj` and `clj->js`, taking the fast path
where possible.

In the following example, a thin wrapper produced by `->clj` allows the
use of `update-in` to produce a new JavaScript object, which is accessed
via `->js`:

```clojure
(require '[cljs-bean.core :refer [->clj ->js]])

(let [o #js {:a #js {:b 1}}]
(-> o ->clj (update-in [:a :b] inc) ->js))
;; #js {:a #js {:b 2}}
```
43 changes: 43 additions & 0 deletions doc/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Overview

The `bean` function produces a thin wrapper over JavaScript objects, implementing the map abstraction:

```clojure
(require '[cljs-bean.core :refer [bean]])

(bean #js {:a 1, :b 2})
;; {:a 1, :b 2}
```

If a bean is going to be retained, the object passed
should be effectively immutable, as the resulting bean is backed by the object.

By default, the `bean` function behaves like Clojure’s in that it is not recursive:

```clojure
(bean #js {:a 1, :obj #js {:x 13, :y 17}, :arr #js [1 2 3]})
;; {:a 1, :obj #js {:x 13, :y 17}, :arr #js [1 2 3]}
```

On the other hand, CLSJ Bean provides `->clj` and `->js` converters, which _are_ recursive.

```clojure
(require '[cljs-bean.core :refer [->clj ->js]])

(->clj #js {:a 1, :obj #js {:x 13, :y 17}, :arr #js [1 2 3]})
;; {:a 1, :obj {:x 13, :y 17}, :arr [1 2 3]}
```

You can update an object produced by `->clj`

```clojure
(-> *1 (update-in [:obj :y] inc) (update :arr pop))
;; {:a 1, :obj {:x 13, :y 18}, :arr [1 2]}
```

and the result above can be converted back to JavaScript via a constant time call to `->js`:

```clojure
(->js *1)
;; #js {:a 1, :obj #js {:x 13, :y 18}, :arr #js [1 2]}
```
22 changes: 22 additions & 0 deletions doc/recursive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Recursive Beans

By default, the `bean` function behaves like Clojure’s in that it is not recursive:

```clojure
(bean #js {:a 1, :obj #js {:x 13, :y 17} :vec #js [1 2 3]})
;; => {:a 1, :obj #js {:x 13, :y 17} :vec #js [1 2 3]}
```

Beans can be made to behave more like `js->clj` by supplying `:recursive` `true`:

```clojure
(bean #js {:a 1, :obj #js {:x 13, :y 17} :vec #js [1 2 3]} :recursive true)
;; => {:a 1, :obj {:x 13, :y 17} :vec [1 2 3]}
```

The `->clj` converter, when applied to JavaScript objects, automatically supplies `:recursive true`, so the above can be simplified to

```clojure
(->clj #js {:a 1, :obj #js {:x 13, :y 17} :vec #js [1 2 3]})
;; => {:a 1, :obj {:x 13, :y 17} :vec [1 2 3]}
```
Loading

0 comments on commit a036dad

Please sign in to comment.