Repository: vigetlabs/microcosm
Pull request #284 (2017-04-04 11:38:07)

Add a way to configure default options. Add default options
We frequently pass custom options into Microcosm to configure Domains and Effects with different options based on the environment. For example, an Effect that auto-saves user data:

```javascript
class Repo extends Microcosm {
  setup ({ saveInterval }) {
    // ...
    this.addEffect(Autosave, { saveInterval })
  }
}
```

### Why

#### Ergonomics

It can be cumbersome to chase down the default options, which may be specified as defaults in individual domains/effects. With this release, you may now define defaults using the `defaults` static:

```javascript
class Repo extends Microcosm {
  static defaults = {
    saveInterval: 5000
  }

  setup ({ saveInterval }) {
    // ...
    this.addEffect(Autosave, { saveInterval })
  }
}
```

#### No more null checks

Before this change, setting default options had to occur with the default arguments feature of ES6:

```javascript
class Repo extends Microcosm {
  setup ({ saveInterval=100 }={}) {
    // ...
    this.addEffect(Autosave, { saveInterval })
  }
}
```

This is confusing. We have to handle nully values if a Microcosm isn't instantiated, and additional defaults for each option. These option lists can get long and hard to scan.

### How it works

This takes advantage of the [Class Fields & Static Properties Spec](https://github.com/tc39/proposal-class-public-fields). This specification is still at [Stage 2](http://2ality.com/2015/11/tc39-process.html), however it has become common place to use this feature within React projects. If living on the edge isn't your thing, defaults may also be configured by assigning a `default` property to your Microcosm subclass:

```javascript
class Repo extends Microcosm {
  setup ({ saveInterval }) {
    // ...
    this.addEffect(Autosave, { saveInterval })
  }
}


Repo.defaults = {
  saveInterval: 5000
}
```

All Microcosm specific options, such as `maxHistory` will get merged into your custom defaults upon construction.

codecov-io: # [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=h1) Report
> Merging [#284](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=desc) into [master](https://codecov.io/gh/vigetlabs/microcosm/commit/dd452bee31e140b073aa9d74eba332ed8e2e5bd8?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/vigetlabs/microcosm/pull/284/graphs/tree.svg?src=pr&height=150&width=650&token=AaE0FLz9sU)](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #284   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          22     22           
  Lines         770    771    +1     
=====================================
+ Hits          770    771    +1
```


| [Impacted Files](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/microcosm.js](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=tree#diff-c3JjL21pY3JvY29zbS5qcw==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=footer). Last update [dd452be...1899959](https://codecov.io/gh/vigetlabs/microcosm/pull/284?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ef71aa7..31b1c3fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,72 @@
 # Changelog
 
+## Edge
+
+- Added a `defaults` static to Microcosm that passes default options
+  to the constructor and setup method.
+- The first argument of `setup`, the options object passed when
+  instantiating a Microcosm argument, will always be an object. There
+  is no need to handle the null case for options.
+
+### Defaults
+
+We frequently pass custom options into Microcosm to configure Domains
+and Effects with different options based on the environment. For
+example, an Effect that auto-saves user data:
+
+```javascript
+class Repo extends Microcosm {
+  setup ({ saveInterval }) {
+    // ...
+    this.addEffect(Autosave, { saveInterval })
+  }
+}
+```
+
+It can be cumbersome to chase down the default options, which may be
+specified as defaults in individual domains/effects. With this
+release, you may now define defaults using the `defaults` static:
+
+```javascript
+class Repo extends Microcosm {
+  static defaults = {
+    saveInterval: 5000
+  }
+
+  setup ({ saveInterval }) {
+    // ...
+    this.addEffect(Autosave, { saveInterval })
+  }
+}
+```
+
+This takes advantage of
+the
+[Class Fields & Static Properties Spec](https://github.com/tc39/proposal-class-public-fields). This
+specification is still
+at [Stage 2](http://2ality.com/2015/11/tc39-process.html), however it
+has become common place to use this feature within React projects. If
+living on the edge isn't your thing, defaults may also be configured
+by assigning a `default` property to your Microcosm subclass:
+
+```javascript
+class Repo extends Microcosm {
+  setup ({ saveInterval }) {
+    // ...
+    this.addEffect(Autosave, { saveInterval })
+  }
+}
+
+
+Repo.defaults = {
+  saveInterval: 5000
+}
+```
+
+All Microcosm specific options, such as `maxHistory` will get merged
+into your custom defaults upon construction.
+
+
 ## 12.4.0
 
 - Added `repo.history.wait()` and `repo.history.then()` to allow tests
diff --git a/docs/api/microcosm.md b/docs/api/microcosm.md
index ba734303..29945920 100644
--- a/docs/api/microcosm.md
+++ b/docs/api/microcosm.md
@@ -276,3 +276,29 @@ fork.addDomain('page', PaginatedPeople)
 // history as the parent
 fork.push(getPeople)
 ```
+
+### `Microcosm.defaults`
+
+Specifies default options a Microcosm is instantiated with. This
+provides a concise way to configure sensible defaults for setup
+options:
+
+```javascript
+class Repo extends Microcosm {
+  static defaults = {
+    saveInterval: 5000
+  }
+
+  setup ({ saveInterval }) {
+    // ...
+    this.addEffect(Autosave, { saveInterval })
+  }
+}
+```
+
+When instantiated, default options are determined in the following
+order:
+
+1. Microcosm defaults
+2. Subclass defaults
+3. Instantiation options
diff --git a/src/microcosm.js b/src/microcosm.js
index 7fce2b73..65503166 100644
--- a/src/microcosm.js
+++ b/src/microcosm.js
@@ -23,12 +23,12 @@ import {
   update
 } from './utils'
 
-function Microcosm (options, state, deserialize)  {
+function Microcosm (preOptions, state, deserialize)  {
   Emitter.call(this)
 
-  options = options || {}
+  let options = merge(Microcosm.defaults, this.constructor.defaults, preOptions)
 
-  this.parent = options.parent || null
+  this.parent = options.parent
 
   this.history = this.parent ? this.parent.history : new History(options.maxHistory)
   this.history.addRepo(this)
@@ -50,6 +50,16 @@ function Microcosm (options, state, deserialize)  {
   }
 }
 
+/**
+ * Options passed into Microcosm always extend from this static
+ * property. You can override this value to provide additional
+ * defaults for your extension of Microcosm.
+ */
+Microcosm.defaults = {
+  maxHistory: 0,
+  parent: null
+}
+
 inherit(Microcosm, Emitter, {
 
   setup () {


Test patch:
diff --git a/test/unit/microcosm/constructor.test.js b/test/unit/microcosm/constructor.test.js
index fc3e5fe8..0e846c28 100644
--- a/test/unit/microcosm/constructor.test.js
+++ b/test/unit/microcosm/constructor.test.js
@@ -28,25 +28,95 @@ describe('Microcosm constructor', function () {
     expect(repo).toHaveState('foo', 'bar')
   })
 
-  describe('option: maxHistory', function () {
+  describe('options', function () {
 
-    it('controls how many transactions are merged', function () {
-      const repo = new Microcosm({ maxHistory: 5 })
-      const identity = n => n
+    describe('parent', function () {
 
-      repo.push(identity, 1)
-      repo.push(identity, 2)
-      repo.push(identity, 3)
-      repo.push(identity, 4)
-      repo.push(identity, 5)
+      it('has no parent by default', function () {
+        expect.assertions(1)
 
-      expect(repo.history.size).toEqual(5)
+        class Repo extends Microcosm {
+          setup ({ parent }) {
+            expect(parent).toEqual(null)
+          }
+        }
 
-      repo.push(identity, 6)
+        new Repo()
+      })
+    })
+
+    describe('maxHistory', function () {
+
+      it('has no history by default', function () {
+        expect.assertions(1)
+
+        class Repo extends Microcosm {
+          setup ({ maxHistory }) {
+            expect(maxHistory).toEqual(0)
+          }
+        }
+
+        new Repo()
+      })
+
+      it('controls how many transactions are merged', function () {
+        const repo = new Microcosm({ maxHistory: 5 })
+        const identity = n => n
+
+        repo.push(identity, 1)
+        repo.push(identity, 2)
+        repo.push(identity, 3)
+        repo.push(identity, 4)
+        repo.push(identity, 5)
+
+        expect(repo.history.size).toEqual(5)
+
+        repo.push(identity, 6)
+
+        expect(repo.history.size).toEqual(5)
+        expect(repo.history.root.payload).toEqual(2)
+        expect(repo.history.head.payload).toEqual(6)
+      })
+
+    })
+
+    describe('extension', function () {
+
+      it('extends custom defaults with Microcosm defaults', function () {
+        expect.assertions(2)
+
+        class Repo extends Microcosm {
+          static defaults = {
+            test: true
+          }
+
+          setup (options) {
+            expect(options.maxHistory).toBe(0)
+            expect(options.test).toBe(true)
+          }
+        }
+
+        new Repo()
+      })
+
+      it('extends custom defaults with passed arguments', function () {
+        expect.assertions(3)
+
+        class Repo extends Microcosm {
+          static defaults = {
+            test: true
+          }
+
+          setup (options) {
+            expect(options.maxHistory).toBe(10)
+            expect(options.test).toBe(true)
+            expect(options.instantiated).toBe(true)
+          }
+        }
+
+        new Repo({ maxHistory: 10, instantiated: true })
+      })
 
-      expect(repo.history.size).toEqual(5)
-      expect(repo.history.root.payload).toEqual(2)
-      expect(repo.history.head.payload).toEqual(6)
     })
 
   })
diff --git a/test/unit/microcosm/setup.test.js b/test/unit/microcosm/setup.test.js
index 990a7ab7..7acbbd9e 100644
--- a/test/unit/microcosm/setup.test.js
+++ b/test/unit/microcosm/setup.test.js
@@ -2,16 +2,29 @@ import Microcosm from '../../../src/microcosm'
 
 describe('Microcosm::setup', function() {
 
-  it('passes options from instantiation', function () {
-    const test = jest.fn()
+  it('extends defaults with options passed from instantiation', function () {
+    expect.assertions(2)
 
     class Repo extends Microcosm {
-      get setup () {
-        return test
+      setup (options) {
+        expect(options.foo).toEqual('bar')
+        expect(options.maxHistory).toEqual(0)
       }
     }
 
-    expect(new Repo({ foo: 'bar' }).setup).toHaveBeenCalledWith({ foo: 'bar' })
+    new Repo({ foo: 'bar' })
+  })
+
+  it('receives an options object, even when it is not passed', function () {
+    expect.assertions(1)
+
+    class Repo extends Microcosm {
+      setup (options) {
+        expect(options).toBeDefined()
+      }
+    }
+
+    new Repo()
   })
 
 })