ducks
Version:
🦆🦆🦆 Ducks is a Reducer Bundles Manager that Implementing the Redux Ducks Modular Proposal with Great Convenience.
216 lines (187 loc) • 6.44 kB
text/typescript
/**
* Ducks - https://github.com/huan/ducks
*
* @copyright 2020 Huan LI (李卓桓) <https://github.com/huan>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import assert from 'assert'
import type {
ActionCreator,
AnyAction,
Reducer,
} from 'redux'
import type { Duck } from './duck.js'
export interface MapObject {
[key: string]: any,
}
const getLogger = (debug: boolean) => (msg: string) => {
if (debug) {
console.info(msg)
}
}
let log: ReturnType<typeof getLogger>
/**
* Huan(202005) TODO:
* Testing static types in TypeScript
* https://2ality.com/2019/07/testing-static-types.html
*
* use tsd? tsdlint?
*
* To be added in the future:
* 1. check selectors take a state as argument and returns a function
* 2. check operations take a store as argument and returns a function
* 3. Conditional Type Checks - https://github.com/dsherret/conditional-type-checks
*/
function validateDuck <D extends Duck> (duck: D, debug = false) {
log = getLogger(debug)
validateActions(duck.actions)
validateOperations(duck.operations)
validateReducer(duck.default)
validateSelectors(duck.selectors)
validateTypes(duck.types)
/**
* Middlewares
*/
validateMiddlewares(duck.middlewares)
validateEpics(duck.epics)
validateSagas(duck.sagas)
validateSetDucks(duck.setDucks)
}
/**
*
* Sub Validating Functions
*
*/
function validateReducer <T extends Reducer> (reducer: T) {
assert.strictEqual(typeof reducer, 'function', 'default export should be function')
// const func = (a = 42, b) => a + b
// func.length === 0
// assert.strictEqual(reducer.length, 2, 'reducer should has two arguments. reducer name: ' + reducer.name + ' arguments length: ' + reducer.length + ' reducer function: ' + reducer.toString())
}
function validateActions <T extends MapObject> (actions: T) {
assert.ok(actions, 'should exported actions')
for (const [key, val] of Object.entries(actions)) {
log('validateActions: validating: ' + key + ', ' + val)
validateString(key)
validateActionType(val)
}
}
function validateOperations <T extends MapObject> (operations?: T) {
if (!operations) {
return
}
assert.ok(operations, 'should exported operations')
Object.keys(operations).forEach(validateString)
Object.values(operations).forEach(validateOperationType)
}
function validateSelectors <T extends MapObject> (selectors?: T) {
if (!selectors) {
return
}
assert.ok(selectors, 'should exported selectors')
Object.keys(selectors).forEach(validateString)
Object.values(selectors).forEach(validateSelectorType)
}
function validateTypes <T extends MapObject> (types?: T) {
if (!types) {
return
}
assert.ok(types, 'should exported types')
Object.keys(types).forEach(validateString)
Object.values(types).forEach(validateString)
}
function validateMiddlewares <T extends MapObject> (middlewares?: T) {
if (!middlewares) {
return
}
Object.keys(middlewares).forEach(validateString)
Object.values(middlewares).forEach(validateMiddlewareType)
}
function validateSagas <T extends MapObject> (sagas?: T) {
if (!sagas) {
return
}
Object.keys(sagas).forEach(validateString)
Object.values(sagas).forEach(validateSagaType)
}
function validateEpics <T extends MapObject> (epics?: T) {
if (!epics) {
return
}
Object.keys(epics).forEach(validateString)
Object.values(epics).forEach(validateEpicType)
}
function validateSetDucks (setDucks?: Function) {
if (setDucks) {
assert.strictEqual(typeof setDucks, 'function', 'setDucks should be a function')
assert.strictEqual(setDucks.length, 1, 'setDucks() should take 1 argument for the Ducks instance')
}
}
/**
*
* Helper Functions
*
*/
function validateString (value: any) {
assert.strictEqual(typeof value, 'string', 'should be string type: ' + value)
}
function validateActionType (actionCreator: ActionCreator<AnyAction>) {
if (typeof actionCreator === 'object') {
for (const [key, val] of Object.entries(actionCreator)) {
// .cancel will be undefined when not defined.
if (key === 'cancel' && !val) {
continue
}
assertAsyncKey(key)
assertFunction(val)
}
} else {
assertFunction(actionCreator)
}
function assertFunction (creator: any) {
assert.strictEqual(typeof creator, 'function', 'actionCreator is expected to be a function, but we got: ' + typeof creator + ', ' + String(creator))
}
function assertAsyncKey (key: string) {
const ASYNC_KEY_LIST = [
'request',
'success',
'failure',
'cancel',
]
assert(ASYNC_KEY_LIST.includes(key), 'async action creator should with object keys: request/success/failure/cancel. we got: ' + key)
}
}
function validateSelectorType (selector: (...args: any[]) => any) {
assert.strictEqual(typeof selector, 'function', 'selector should be a function')
}
function validateOperationType (operation: (...args: any[]) => any) {
assert.strictEqual(typeof operation, 'function', 'operation should be a function')
}
function validateMiddlewareType (middleware: (...args: any[]) => any) {
assert.strictEqual(typeof middleware, 'function', 'middleware should be a function')
assert.strictEqual(middleware.length, 1, 'middleware should has 1 arguments')
assert.match(middleware.name, /Middleware$/, 'middleware should be named as xxxMiddleware (ends with `Middleware`)')
}
function validateSagaType (saga: (...args: any[]) => any) {
assert.strictEqual(typeof saga, 'function', 'saga should be a function')
assert.match(saga.name, /Saga$/, 'saga should be named as xxxSaga (ends with `Saga`)')
}
function validateEpicType (epic: (...args: any[]) => any) {
assert.strictEqual(typeof epic, 'function', 'epic should be a function')
assert.match(epic.name, /Epic$/, 'epic should be named as xxxEpic (ends with `Epic`)')
}
export {
validateDuck,
}