Navigation Menu

Skip to content

Commit

Permalink
FIRST
Browse files Browse the repository at this point in the history
  • Loading branch information
danielearwicker committed Jul 22, 2016
1 parent fc2dacd commit 90343ea
Show file tree
Hide file tree
Showing 9 changed files with 898 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
built
node_modules
.DS_Store
.vscode
Empty file added .npmignore
Empty file.
11 changes: 11 additions & 0 deletions .travis.yml
@@ -0,0 +1,11 @@
language: node_js
node_js:
- "stable"
- "4.1"
- "4.0"
- "0.12"
- "0.11"
- "0.10"
before_install:
- npm install -g typescript
- npm install -g jasmine
140 changes: 140 additions & 0 deletions index.ts
@@ -0,0 +1,140 @@
import { Map } from "immutable";
import { createStore, Unsubscribe } from "redux"

export interface Action<Type, Payload> {
type: Type,
payload: Payload
}

export function action<State, Type, Payload>(
type: Type,
reduce: (state: State, payload: Payload) => State) {

function create(payload: Payload) {
return { type: type, payload };
}

const meta = { type, reduce };
const result: typeof create & typeof meta = create as any;
result.type = type;
result.reduce = reduce;

return result;
}

export interface Store<State, ActionType> {
dispatch(action: ActionType): void;
getState(): State;
subscribe(listener: () => void): Unsubscribe;
}

export interface Cursor<State, ActionType> {
(action: ActionType): void;
(): State;
}

export function createStoreCursor<State, ActionType>(
store: Store<State, ActionType>
): Cursor<State, ActionType> {

const dummyState = {} as State;

return (action?: ActionType) => {
if (action) {
store.dispatch(action);
return dummyState;
}
return store.getState();
};
}

export interface Reducer<State, Types extends Action<string, any>> {

(state: State, action: Types): State;

add<TypeName extends string, Payload>(action: {
type: TypeName,
reduce: (state: State, action: Payload) => State
}): Reducer<State, Types | Action<TypeName, Payload>>;

createStore(): Store<State, Types>;

readonly actionType: Types;
readonly cursorType: Cursor<State, Types>
}

export function reducer<State>(init: State) {

type ActionMap<State> = Map<string, (state: State, payload: any) => State>;

function combine<State, Types extends Action<string, any>>(
init: State,
map: ActionMap<State>
): Reducer<State, Types> {

function reduce(state: State, action: Types) {
if (state === undefined) {
return init;
}

var handler = map.get(action.type as any);
if (!handler) {
throw new Error(`Unrecognised action: ${action.type}`);
}
return handler(state, action.payload);
}

function add<TypeName extends string, Payload>(action: {
type: TypeName,
reduce: (state: State, payload: Payload) => State
}) {
const type = action.type as string;
if (map.has(type)) {
throw new Error(`Duplicate action: ${type}`);
}

return combine<State, Types | Action<TypeName, Payload>>(
init, map.set(type, action.reduce));
};

function store() {
return createStore(reduce);
}

const meta = {
add,
createStore: store,
actionType: {} as Types,
cursorType: {} as Cursor<State, Types>
};

const result: typeof reduce & typeof meta = reduce as any;
result.add = add;
result.createStore = store;

return result;
}

return combine<State, never>(init, Map<any, any>());
}

export function defineCursor<ContainerState, ContainerActionType, Address, TargetState, TargetActionType>(
fetch: (container: ContainerState, address: Address) => TargetState,
update: (address: Address, action: TargetActionType) => ContainerActionType
) {
const dummyState = {} as TargetState;

return (container: Cursor<ContainerState, ContainerActionType>,
address: Address, snapshot?: boolean): Cursor<TargetState, TargetActionType> => {

const snapshotValue = snapshot ? fetch(container(), address) : dummyState;

return (targetAction?: TargetActionType) => {
if (targetAction) {
container(update(address, targetAction));
return dummyState;
}
return snapshot ? snapshotValue : fetch(container(), address);
};
};
}
29 changes: 29 additions & 0 deletions package.json
@@ -0,0 +1,29 @@
{
"name": "immuto",
"version": "0.9.0",
"description": "Very strongly typed Redux adaptation for TypeScript",
"main": "built/index.js",
"typings": "built/index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/danielearwicker/immuto.git"
},
"keywords": [
"redux",
"react",
"typescript"
],
"author": "Daniel Earwicker <dan@earwicker.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/danielearwicker/immuto/issues"
},
"homepage": "https://github.com/danielearwicker/immuto#readme",
"dependencies": {
"immutable": "^3.8.1",
"redux": "^3.5.2"
}
}
180 changes: 180 additions & 0 deletions spec/immutoSpec.ts
@@ -0,0 +1,180 @@
import { Action, Reducer, Store, action, reducer, defineCursor, createStoreCursor } from "../index";
import { List } from "immutable";

export class Book {

constructor(
public readonly title: string,
public readonly price: number,
public readonly authors: List<string>) {
}

static readonly setTitle = action<Book, "SET_TITLE", string>(
"SET_TITLE", (book, title) => new Book(title, book.price, book.authors));

static readonly setPrice = action<Book, "SET_PRICE", number>(
"SET_PRICE", (book, price) => new Book(book.title, price, book.authors));

static readonly addAuthor = action<Book, "SET_AUTHOR", string>(
"SET_AUTHOR", (book, author) => new Book(book.title, book.price, book.authors.push(author)));

static readonly reduce = reducer<Book>(new Book("", 0, List<string>()))
.add(Book.setTitle)
.add(Book.setPrice)
.add(Book.addAuthor);
}

class Shelf {

constructor(
public readonly description: string,
public readonly books: List<Book>) {
}

static readonly setDescription = action<Shelf, "SET_DESCRIPTION", string>(
"SET_DESCRIPTION", (shelf, descr) => new Shelf(descr, shelf.books));

static readonly addBook = action<Shelf, "ADD_BOOK", string>(
"ADD_BOOK", (shelf, title) => new Shelf(shelf.description, shelf.books.push(new Book(title, 0, List<string>()))));

static readonly updateBook = action<Shelf, "UPDATE_BOOK", [number, typeof Book.reduce.actionType]>(
"UPDATE_BOOK", (shelf, args) => new Shelf(shelf.description, shelf.books.update(args[0], b => Book.reduce(b, args[1]))));

static readonly reduce = reducer<Shelf>(new Shelf("", List<Book>()))
.add(Shelf.setDescription)
.add(Shelf.addBook)
.add(Shelf.updateBook);

static readonly bookAt = defineCursor(
(shelf: Shelf, pos: number) => shelf.books.get(pos),
(pos: number, bookAction: typeof Book.reduce.actionType) => Shelf.updateBook([pos, bookAction]));
}

class Shop {
constructor(
public readonly name: string,
public readonly shelves: List<Shelf>) {
}

static readonly setName = action<Shop, "SET_NAME", string>(
"SET_NAME", (shop, name) => new Shop(name, shop.shelves));

static readonly addShelf = action<Shop, "ADD_SHELF", string>(
"ADD_SHELF", (shop, name) => new Shop(shop.name, shop.shelves.push(new Shelf(name, List<Book>()))));

static readonly updateShelf = action<Shop, "UPDATE_SHELF", [number, typeof Shelf.reduce.actionType]>(
"UPDATE_SHELF", (shop, args) => new Shop(shop.name, shop.shelves.update(args[0], s => Shelf.reduce(s, args[1]))));

static readonly reduce = reducer<Shop>(new Shop("", List<Shelf>()))
.add(Shop.setName)
.add(Shop.addShelf)
.add(Shop.updateShelf);

static readonly shelfAt = defineCursor(
(shop: Shop, pos: number) => shop.shelves.get(pos),
(pos: number, shelfAction: typeof Shelf.reduce.actionType) => Shop.updateShelf([pos, shelfAction]));
}

const enableLogging = false;

function logStore<State, Types>(store: Store<State, Types>) {
if (enableLogging) {
store.subscribe(() => {
console.log("");
console.log(JSON.stringify(store.getState()));
console.log("");
});
}
return store;
}

describe("immuto", () => {

it("has an initial state available via cursor", () => {

const book = createStoreCursor(logStore(Book.reduce.createStore()))();

expect(book.title).toEqual("");
expect(book.price).toEqual(0);
expect(book.authors.count()).toEqual(0);

expect(JSON.stringify(book)).toEqual(`{"title":"","price":0,"authors":[]}`);
});

it("can be updated via late-bound cursors", () => {

const shelf = createStoreCursor(logStore(Shelf.reduce.createStore()));
shelf(Shelf.setDescription("Romance"));

expect(JSON.stringify(shelf())).toEqual(`{"description":"Romance","books":[]}`);

// Create cursor here, before adding book!
const firstBook = Shelf.bookAt(shelf, 0);

shelf(Shelf.addBook("1985"));

expect(JSON.stringify(shelf())).toEqual(`{"description":"Romance","books":[{"title":"1985","price":0,"authors":[]}]}`);

expect(firstBook().title).toEqual("1985");
expect(firstBook().price).toEqual(0);

firstBook(Book.setPrice(5.99));

expect(JSON.stringify(shelf())).toEqual(`{"description":"Romance","books":[{"title":"1985","price":5.99,"authors":[]}]}`);

expect(firstBook().price).toEqual(5.99);
});

it("can be updated via snapshot cursors", () => {

const shelf = createStoreCursor(logStore(Shelf.reduce.createStore()));
shelf(Shelf.setDescription("Romance"));
shelf(Shelf.addBook("1985"));

expect(JSON.stringify(shelf())).toEqual(`{"description":"Romance","books":[{"title":"1985","price":0,"authors":[]}]}`);

// ask for snapshot cursor, so have to do it after adding book
const firstBook = Shelf.bookAt(shelf, 0, true);

expect(firstBook().title).toEqual("1985");
expect(firstBook().price).toEqual(0);

firstBook(Book.setPrice(5.99));

expect(JSON.stringify(shelf())).toEqual(`{"description":"Romance","books":[{"title":"1985","price":5.99,"authors":[]}]}`);

// firstBook is a snapshot so doesn't see the change
expect(firstBook().price).toEqual(0);

// take another shapshot
const firstBookAgain = Shelf.bookAt(shelf, 0, true);
expect(firstBookAgain().price).toEqual(5.99);
});

it("supports nested layers of cursors", () => {

const shop = createStoreCursor(logStore(Shop.reduce.createStore()));
shop(Shop.setName("Buy the Book, Inc."));

shop(Shop.addShelf("Adventure"));

const firstShelf = Shop.shelfAt(shop, 0);
expect(firstShelf().description).toEqual("Adventure");

firstShelf(Shelf.addBook("Indiana Smith"));

expect(JSON.stringify(shop())).toEqual(`{"name":"Buy the Book, Inc.","shelves":[{"description":"Adventure","books":[{"title":"Indiana Smith","price":0,"authors":[]}]}]}`);

const firstBookOfFirstShelf = Shelf.bookAt(firstShelf, 0);
expect(firstBookOfFirstShelf().title).toEqual("Indiana Smith");

firstBookOfFirstShelf(Book.setPrice(4.99));
firstBookOfFirstShelf(Book.addAuthor("Jim Orwell"));

expect(JSON.stringify(shop())).toEqual(`{"name":"Buy the Book, Inc.","shelves":[{"description":"Adventure","books":[{"title":"Indiana Smith","price":4.99,"authors":["Jim Orwell"]}]}]}`);

expect(firstBookOfFirstShelf().title).toEqual("Indiana Smith");
expect(firstBookOfFirstShelf().price).toEqual(4.99);
expect(firstBookOfFirstShelf().authors.first()).toEqual("Jim Orwell");
});
});

0 comments on commit 90343ea

Please sign in to comment.