Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

README attention #22

Merged
merged 14 commits into from
Mar 25, 2015
164 changes: 43 additions & 121 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Matchbox

Firebase bindings for Clojure(Script)
![Matchbox - Firebase bindings for Clojure(script)](https://cloud.githubusercontent.com/assets/881351/6807041/c6d72cbe-d254-11e4-896f-b75d2153d197.png)

[![Build Status](https://travis-ci.org/crisptrutski/matchbox.svg?branch=master)](https://travis-ci.org/crisptrutski/matchbox)
[![Dependency Status](https://www.versioneye.com/clojure/matchbox:matchbox/badge.svg)](https://www.versioneye.com/clojure/matchbox:matchbox)
Expand All @@ -9,132 +7,42 @@ Firebase bindings for Clojure(Script)

[![Clojars Project](http://clojars.org/matchbox/latest-version.svg)](http://clojars.org/matchbox)


# Features

Matchbox offers more than just bindings:

* Atom/Zipper/Cursor-ish abstraction over Firebase references
* Clojure data in/out
* Cursor/Atom-like abstraction over Firebase references
* Nested versions of all operations
* Uniform API for JVM and JS platforms
* Optional core.async based API
* Multiplexed event channels and/or callbacks


# Example usage


```clojure
;; Load and alias
(require '[matchbox.core :as m])

;; Create a printer (this is platform dependent)

;; cljs
(enable-console-print!)
(def safe-prn (partial prn "> "))

;; clj
(def safe-prn (partial matchbox.utils/prn "> "))

;; Get a reference the root (update the URI with your app's name)
(def r (m/connect "https://<your-app>.firebaseio.com/"))

;; Let's write some data
(m/reset! r {:deep {:route "secret"}})

;; Let's read that data back
;; Note: We'll get a tuple with the parent key too, or nil if root
(m/deref r safe-prn) ;; => [nil {:deep {:route "secret"}}]

;; We can also pass a callback, eg. to know once the value is persisted
;; The callback will receive the reference written to.
(m/reset! r {:deep {:route "better-secret"}} safe-prn)

;; We can get child references, and read just that portion
;; Note: going forward I'm omitting key portion in output comments
(def child (m/get-in r [:deep :route]))
(m/deref child safe-prn) ;; => "better-secret"

;; Where it makes sense, you can add `-in` to the operation name to
;; operate on children directly through their parent reference.
;;
;; These variants take an extra argument, in second position, for the path
(m/reset-in! r [:deep :route] "s3krit")
(m/deref-in r [:deep :route] safe-prn)
* Multiplexed children event channels and/or callbacks

;; Note that paths are very forgiving, keywords and strings work also:
(m/deref-in r :deep safe-prn) ;; => {:route "s3krit"}
(m/deref-in r "deep/route" safe-prn) ;; => "s3krit"
# Usage

;; Now lets add a persistent listener, so we can keep abreast of changes to our root:
(m/listen-to r :value (partial safe-prn "listener: "))
Take a look at the [quick intro](docs/quick_intro.cljx) to for a lightning tour.

;; Lets see how else we can mutate data
(m/reset! r {:something "else"}) ;; => Note that :deep was lost
(m/merge! r {:less "extreme"}) ;; => This time :something is retained
(m/conj! r "another value") ;; => A "strange" key was generated
(def child (m/get-in r :less))
(defn rev-str [x] (apply str (reverse x)))
(m/swap! child rev-str) ;; => Like an atom - value is now "emertxe"
(m/remove! child) ;; => Only :less was removed
## ClojureScript demos

;; Note that `remove!` has an alias `dissoc!`, to fit with `-in` case:
(m/dissoc-in! r [:something]) ;; => Only :something was removed
There are some demos in the `examples` directory.

;; If you're wondering about the random key created by `conj!`, you may want to
;; read this: https://www.firebase.com/blog/2014-04-28-best-practices-arrays-in-firebase.html
Those in the `cljs` folder can be compiled from the main project via `lein cljsbuild once <example-name>`, and then run by opening the 'index.html' found in the example directory in a browser.

;; Coming from writes back to observers, let's take a deeper look at `listen-to`
;; TODO: add note and example about other event types and how it hadnles -in case

;; There is also listen-children, which multiplexes across all children events
;; TODO: add an example

;; TODO: provide notes and examples around using "priority"

;; TODO: provide notes and examples around using auth

;; Lets look quickly at the core.async variants of the various functions
(:require [matchbox.async :as ma])

;; TODO: mention which ops are in async namespace, the naming convention (`<`, `-in<`)

;; TODO: update this example to fit into narrative, and add some around other ops
(let [c (ma/listen-to< r :messages :child_added)]
(go-loop [msg (<! c)]
(.log js/console "New message (go-loop):" (:message msg))
(recur (<! c))))

```

## ClojureScript Examples

All examples are available under the `examples` directory. To run a Clojurescript example just run the respective `lein` command to build it:

```clojure
lein cljsbuild once <example-name>
```


This should build and place a `main.js` file along with an `out` directory in the example's directory.

You should now be able to go to the example's directory and open the
`index.html` file in a web-browser.
There is also a stand-alone demo project, `reagent-example`. This example can be launched by executing `lein run`, and opening "http://localhost:3000" in a browser.

## Gotchas

1. Swap! takes callback in non-standard way

Since it's common to pass additional arguments to an update function,
a pragmatic choice was made. We try so support both, by aluding to the
common inline-keywords style supported by `& {...}` destructuring:
Since we support passing additional arguments to an update function,
we can't use an optional argument for the callback.

Our solution draws inspiration from "kwargs" style signatures:

```clojure
(eg. `(my-function :has "keyword" :style "arguments")`).
```

In our case, we allow `:callback callback-fn` at the end of the args, eg:
Coming back to `swap!`, we support `:callback callback-fn` at end of arg list:

```clojure
(m/swap! r f) ;; call (f <val>), no callback
Expand All @@ -145,43 +53,57 @@ You should now be able to go to the example's directory and open the

Note that `:callback` MUST appear as the second-last argument.

2. Java callbacks
2. JVM callbacks on side thread

Depending on your [environment and
config](https://www.firebase.com/docs/java-api/javadoc/com/firebase/client/Config.html#setEventTarget(com.firebase.client.EventTarget)),
callbacks may be triggered on another thread.

This can be confusing if debugging with `prn` in callbacks, as
`*out*` will not be to the REPL's writer. We define `matchbox.utils/prn` as a simple
helper to ensure output is visible.
This can be confusing when debugging with `prn` in callbacks, as
`*out*` will not be to the REPL's writer. We provide `matchbox.utils/prn` as a simple
helper to ensure output is visible.

3. Serialization

Data | Storage | Reads back as it writes?
--- | --- | ---
`{}`, nameable keys | JSON | Not unless all keys are keywords (rest are coerced)
`{}`, richer keys | Not supported | N/A
`[]` | JSON with numeric keys | Yes
`#{}` | JSON with numeric keys | No, reads back as a vector
`"string"` | string | Yes
`:a/keyword` | ":a/keyword" | Yes
Number | Number | Pretty much, with nits for `java.math.*` types
Record | JSON | No, reads back as vanilla map
(def)Type | JSON | No, reads back as vanilla map
Other | Depends on platform | Expect useless strings (JS) or serious downcasting (JVM)

Since Firebase is a JSON-like store, we automatically convert keys in nested
maps to strings. No metadata about initial type is stored, and keys are
always read as keywords.

Maps are not restricted to just keywords and strings though, but the case of
rich keyws has not been handled. On the JVM passing using such values will
result in a `ClassCastExceoption`, and in JS you can always cast, so you'll
rich keys has not been handled. On the JVM passing using such values will
result in a `ClassCastException`, and in JS you can always cast, so you'll
pull back a keyword, like `:32` or perhaps `:[object Object]`.

Coming to values, you're at the mercy of the platform and little work has
been done to manage the semantics. Luckily they work pretty well, but less
fortunately they are inconsistent between the two platforms.
been done to manage the semantics. For basic data types they are at least
consistent between platforms, and mostly lossless or "almost-lossless".

We leverage `java.util.Collection` and `cljs->js` respectively between the
JVM and JS. That means that almost everything becomes an array, and most
primitives stay the same. The most notable difference is that `cljs->js`
turns keywords into strings, but we're making no such cast on the JVM.

Strings and boolean are stable, and stored using the corresponding
primitives. Keywords are also stable, but are stored as colon-prefixed
strings, which are "auto-magically" rehydrated.
Strings, booleans and keywords are stable, and stored either as the
associated JSON type, or as an EDN string in the case of keywords. We
may support symbols, dates etc also as EDN in future.

Numbers are stable in JS. On the JVM Numbers are stable for the core cases of
Long and Double, although more exotic types like `java.math.BigDec` will be
cast down.
Numbers are mostly stable. For JS, floats-only is a gotcha but par for the course.
On the JVM Numbers are stable for the core cases of Long and Double, although more
exotic types like `java.math.BigDec` will be cast down. One strange behaviour is that while
`4.0` will read back as a Double, `4M` will read back as a Long.

Records are saved as regular maps. Types defined with `deftype` also cast
down to maps, but their attributes for some reason are `under_scored` rather
Expand Down
Loading