Skip to content

Commit

Permalink
Improve documentation a little bit
Browse files Browse the repository at this point in the history
  • Loading branch information
Piotr Oleś committed Jun 24, 2017
1 parent 71da0c3 commit a3ce1ca
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 112 deletions.
135 changes: 27 additions & 108 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,60 +21,14 @@ is no way to put additional logic in these models.

Immutable Models wraps immutable structures and provides interfaces defined by a developer.

## Usage
### IterableModel
The basic class of this package is `IterableModel<I extends Iterable<any, any>>`. It's a simple wrapper for any iterable
from Immutable.js.

Let's say we want to create well defined `ModelHistory` model - see how simple it is:

```typescript
import { Stack } from 'immutable';
import { IterableModel } from 'immutable-models';

export class ModelHistory<T> extends IterableModel<Stack<T>> {
constructor(stack = Stack<T>()) {
super(stack);
}

getCurrent(): T {
return this.data.peek();
}

applyChange(model: T): this {
return this.update(data => data.push(model));
}

undoChange(): this {
return this.update(data => data.pop());
}

commitChanges(): this {
return this.update(data => data.take(1).toStack());
}

rollbackChanges(): this {
return this.update(data => data.takeLast(1).toStack());
}
}
```

`IterableModel` implements `ValueObject` interface so it works well with `Immutable.is` function - content, not reference means.

### Model
More common use-cases are classic models like `User` or `Post`. That's why we've created another class - `Model<T>`.
Basically it extends `IterableModel<Map<T>>` and provides some shortcut methods.

Example of user model:
Let's see some example:
```typescript
import { Model } from 'immutable-models';
import { Permission } from './Permission';
import { Set } from 'immutable';

interface UserShape {
userName: string;
firstName?: string;
lastName?: string;
userName: string
createdBy?: User;
permissions?: Set<Permission>;
}
Expand All @@ -83,21 +37,17 @@ export class User extends Model<UserShape> {
getUserName(): string {
return this.get('userName');
}

getFirstName(): string {
return this.get('firstName');
}

getLastName(): string {
return this.get('lastName');

getCreatedBy(): User {
return this.get('createdBy');
}

getFullName(): string {
return `${this.getFirstName()} ${this.getLastName()}`;
isAdmin(): boolean {
return this.hasPermission(Permission.ADMIN);
}

getCreatedBy(): User {
return this.get('createdBy');
isCreatedByAdmin(): boolean {
return this.getCreatedBy() ? this.getCreatedBy().isAdmin() : false;
}

getPermissions(): Set<Permission> {
Expand All @@ -112,58 +62,27 @@ export class User extends Model<UserShape> {
return this.update('permissions', permissions => permissions.add(permission));
}
}
```

### ReadonlyModel
If you want to store a lot of models, for example timeserie entry models, and you don't have to "update" it,
we suggest to use simpler `ReadonlyModel<T>`.

Same `User` class would look like:
```typescript
import { ReadonlyModel } from 'immutable-models';
import { Permission } from './Permission';
import { Set } from 'immutable';
// example usage
const user = new User({
userName: 'piotr',
permissions: Set([Permission.DEVELOPER])
});
user.getUserName(); // > piotr
user.hasPermission(Permission.ADMIN); // false

// we create adminUser based on user - immutable data
const adminUser = user.addPermission(Permission.ADMIN); // make me an admin!
adminUser.getUserName(); // > piotr
adminUser.hasPermission(Permission.ADMIN); // > true
user.hasPermission(Permission.ADMIN); // false
```
Like you see, `User` class hides complexity of Immutable.js structures and contains business logic.

interface UserShape {
userName: string;
firstName?: string;
lastName?: string;
createdBy?: User;
permissions?: Set<Permission>;
}
## Documentation
It's not completed but we are working on it:
* [API Reference](doc/api/README.md)

export class User extends ReadonlyModel<UserShape> {
getUserName(): string {
return this.get('userName');
}

getFirstName(): string {
return this.get('firstName');
}

getLastName(): string {
return this.get('lastName');
}

getFullName(): string {
return `${this.getFirstName()} ${this.getLastName()}`;
}

getCreatedBy(): User {
return this.get('createdBy');
}

getPermissions(): Set<Permission> {
return this.get('permissions', Set<Permission>());
}

hasPermission(permission: Permission): boolean {
return this.getPermissions().contains(permission);
}
}
```
Like you see, migration between `Model` and `ReadonlyModel` is very easy. Keep in mind, that `ReadonlyModel` doesn't implement
`ValueObject` interface - `Immutable` will check objects references.

## Typings
If you are using [TypeScript](https://www.typescriptlang.org/), you don't have to install typings - they are provided in npm package.
Expand Down
158 changes: 158 additions & 0 deletions doc/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# API Reference

* [`IterableModel`](#iterablemodel)
* [`static getWrappedImmutable(model)`](#iterablemodel-static-getwrappedimmutablemodel)
* [`new (iterable)`](#iterablemodel-new-iterable)
* [`equals(model)`](#valueobject-equals)
* [`hashCode()`](#valueobject-hashcode)
* [`toJSON()`](#iterablemodel-tojson)
* [`#data`](#iterablemodel-protected-data)
* [`#update(updater, ...args)`](#iterablemodel-protected-updateupdater-args)
* [`#memoize(key, getter)`](#iterablemodel-protected-memoizekey-getter)
* [`Model`](#model)
* [`TODO: new (data)`]()
* [`equals(model)`](#valueobject-equals)
* [`hashCode()`](#valueobject-hashcode)
* [`toJSON()`](#iterablemodel-tojson)
* [`TODO: #update(key, updater)`]()
* [`TODO: #get(key, notSetValue)`]()
* [`TODO: #set(key, value)`]()
* [`TODO: #has(key)`]()
* [`TODO: #remove(key)`]()
* [`TODO: #merge(newData)`]()
* [`TODO: #withMutations(mutator)`]()
* [`TODO: ReadonlyModel`]()
* [`TODO: toJSON()`]()
* [`TODO: #get(key, notSetValue)`]()
* [`TODO: #has(key)`]()
* [`TODO: #memoize(key, getter)`]()
* [`TODO: TypedRecord`]()
* [`Interfaces`](#interfaces)
* [`TypedMap`](#typedmap)
* [`ValueObject`](#valueobject)

## `IterableModel`
The basic class of this package is `IterableModel<I extends Iterable<any, any>>`. It's a simple wrapper for any iterable
from Immutable.js (`List`, `Map`, `Set`, etc.). With this class we can create well defined interfaces and hide Immutable.js complexity.
It also implements [`ValueObject`](#valueobject) interface so we can use `Immutable.is` to compare two `IterableModel`s.

#### Example
```typescript
import { Stack } from 'immutable';
import { IterableModel } from 'immutable-models';

export class ModelHistory<T> extends IterableModel<Stack<T>> {
constructor(stack = Stack<T>()) {
super(stack);
}

getCurrent(): T {
return this.data.peek();
}

applyChange(model: T): this {
return this.update(data => data.push(model));
}

undoChange(): this {
return this.update(data => data.pop());
}

commitChanges(): this {
return this.update(data => data.take(1).toStack());
}

rollbackChanges(): this {
return this.update(data => data.takeLast(1).toStack());
}
}
```

### `[IterableModel] static getWrappedImmutable(model)`
Get wrapped Immutable.js iterable from `IterableModel`. Throws error if passed model is not instance of `IterableModel`.

* `model: IterableModel<I>` - model to get iterable from

Returns `I extends Iterable<any, any>` - Iterable wrapped by model

### `[IterableModel] new (iterable)`
* `iterable: I extends Iterable<any, any>` - Immutable.js iterable to wrap.

### `[IterableModel] toJSON()`
Serialize model to native object. By default we use shallow `.toObject` or `.toArray` from Immutable.js - if you need
deep serialization, you have to write it by your own (but probably it's everything you need - `JSON.stringify` calls
`.toJSON` recursively)

#### Note
This method uses memoization to prevent performance issue (for example if you use Redux Devtools, calling
`.toJSON` on every dispatch can be costly).

### `[IterableModel] protected data`
Property which holds wrapped Immutable.js iterable. You can access it directly inside your model.

### `[IterableModel] protected update(updater, ...args)`
The way we can "mutate" iterable model is creating new one with new immutable data. To achieve it, use
`update` method.
* `updater: <I extends Iterable<any, any>>(data: I) => I` - function which returns new iterable data based on
previous one.
* `...args: any[]` - additional args passed to constructor

Returns: `this` - new instance of model with new iterable

#### Note:
There is no typechecking in internal part of update method - if you want to have constructor signature incompatible with
`IterableModel`, you have to modify `update` method.

### `[IterableModel] protected memoize(key, getter)`
To improve time and memory performance we can use memoization - it's immutable so result will be always the same.
* `key: string` - unique key to store memoization
* `getter: <I extends Iterable<any, any>, M>(data: I) => M` - function which should return desired value from iterable data

Returns: `M` - result of getter function.

## `Model`
More common use-cases are classic models like `User` or `Post`. That's why we've created another class - `Model<T>`.
Basically it extends `IterableModel<Map<T>>` and provides few shortcut methods.

#### Example
```typescript
import { Model } from 'immutable-models';
import { Permission } from './Permission';
import { Set } from 'immutable';

interface UserShape {
userName: string
createdBy?: User;
permissions?: Set<Permission>;
}

export class User extends Model<UserShape> {
getUserName(): string {
return this.get('userName');
}

getCreatedBy(): User {
return this.get('createdBy');
}

isAdmin(): boolean {
return this.hasPermission(Permission.ADMIN);
}

isCreatedByAdmin(): boolean {
return this.getCreatedBy() ? this.getCreatedBy().isAdmin() : false;
}

getPermissions(): Set<Permission> {
return this.get('permissions', Set<Permission>());
}

hasPermission(permission: Permission): boolean {
return this.getPermissions().contains(permission);
}

addPermission(permission: Permission): this {
return this.update('permissions', permissions => permissions.add(permission));
}
}
```
15 changes: 11 additions & 4 deletions src/IterableModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ export namespace IterableModel {
}

/**
* IterableModel - wrapper for any Iterable from Immutable.js package (List, Map, Set, etc.).
* Implements ValueObject interface so we can use Immutable.is to compare two IterableModels.
* The basic class of this package is IterableModel<I extends Iterable<any, any>>. It's a simple wrapper for any iterable from Immutable.js
* (List, Map, Set, etc.). With this class we can create well defined interfaces and hide Immutable.js complexity. It also implements
* ValueObject interface so we can use Immutable.is to compare two IterableModels.
*/
export abstract class IterableModel<I extends Iterable<any, any>> implements ValueObject {

/**
* Get wrapped Immutable.js iterable from `IterableModel`. Throws error if passed model is not instance of `IterableModel`.
*/
static getWrappedImmutable<I extends Iterable<any, any>>(model: IterableModel<I>): I {
if (!(model instanceof IterableModel)) {
throw new TypeError('Cannot get wrapped immutable from object that is not a subtype of the IterableModel.');
Expand Down Expand Up @@ -54,8 +58,11 @@ export abstract class IterableModel<I extends Iterable<any, any>> implements Val
}

/**
* "Update" the model - create a new one with data transformed by a updater.
* CAUTION: There is no type check on constructor arguments.
* The way we can "mutate" iterable model is creating new one with new immutable data. To achieve it, use update method.
*
* NOTE:
* There is no typechecking in internal part of update method - if you want to have constructor signature incompatible with IterableModel,
* you have to modify update method.
*/
protected update(updater: IterableModel.Updater<I>, ...args: any[]): this {
return new (Object.getPrototypeOf(this).constructor)(updater(this.data), ...args);
Expand Down

0 comments on commit a3ce1ca

Please sign in to comment.