├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── actions │ └── profile.lua ├── main.lua ├── middlewares │ ├── index.lua │ ├── logger.lua │ └── thunk.lua ├── reducers │ ├── index.lua │ └── profile.lua └── store.lua ├── rockspecs ├── lredux-0.1.0-1.rockspec ├── lredux-0.2.0-1.rockspec ├── lredux-0.3.0-1.rockspec └── redux-lua-0.1.0-1.rockspec ├── spec └── lredux_spec.lua └── src ├── applyMiddleware.lua ├── combineReducers.lua ├── compose.lua ├── createStore.lua ├── env.lua ├── helpers ├── array.lua └── assign.lua ├── null.lua └── utils ├── actionTypes.lua ├── inspect.lua ├── isPlainObject.lua └── logger.lua /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | env: 5 | - LUA="lua=5.1" 6 | - LUA="lua=5.2" 7 | - LUA="lua=5.3" 8 | - LUA="luajit=2.0" 9 | - LUA="luajit=2.1" 10 | 11 | before_install: 12 | - pip install hererocks 13 | - hererocks lua_install -r^ --$LUA 14 | - export PATH=$PATH:$PWD/lua_install/bin 15 | 16 | install: 17 | - luarocks install inspect 18 | - luarocks install luacheck 19 | - luarocks install busted 20 | - luarocks install luacov 21 | - luarocks install luacov-coveralls 22 | 23 | script: 24 | - luacheck --std max+busted spec 25 | - busted --verbose --coverage 26 | 27 | branches: 28 | only: 29 | - master 30 | 31 | notifications: 32 | email: 33 | on_success: change 34 | on_failure: always 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eric Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-lua 2 | [![Build Status](https://api.travis-ci.org/pyericz/redux-lua.svg?branch=master)](https://travis-ci.org/pyericz/redux-lua) 3 | 4 | Originally, [redux](https://redux.js.org/) is a predictable state container for JavaScript apps. From now on, all the redux features are available on your Lua projects. Try it out! :-D 5 | 6 | ## Install 7 | redux-lua can be installed using [LuaRocks](http://luarocks.org/modules/pyericz/redux-lua): 8 | ``` 9 | $ luarocks install redux-lua 10 | ``` 11 | 12 | ## Usage 13 | Here is an example of profile updating. To handle redux state changes, it is recommended to use [redux-props](https://github.com/pyericz/redux-props). To get more usages, please checkout [examples](https://github.com/pyericz/redux-lua/tree/master/examples). 14 | 15 | ### Define actions 16 | ```lua 17 | --[[ 18 | actions/profile.lua 19 | --]] 20 | local actions = {} 21 | 22 | function actions.updateName(name) 23 | return { 24 | type = "PROFILE_UPDATE_NAME", 25 | name = name 26 | } 27 | end 28 | 29 | function actions.updateAge(age) 30 | return { 31 | type = "PROFILE_UPDATE_AGE", 32 | age = age 33 | } 34 | end 35 | 36 | function actions.remove() 37 | return { 38 | type = "PROFILE_REMOVE" 39 | } 40 | end 41 | 42 | function actions.thunkCall() 43 | return function (dispatch, state) 44 | return dispatch(actions.remove()) 45 | end 46 | end 47 | 48 | return actions 49 | ``` 50 | 51 | ### Define reducer 52 | ```lua 53 | --[[ 54 | reducers/profile.lua 55 | --]] 56 | local assign = require 'redux.helpers.assign' 57 | local Null = require 'redux.null' 58 | 59 | local initState = { 60 | name = '', 61 | age = 0 62 | } 63 | 64 | local handlers = { 65 | ["PROFILE_UPDATE_NAME"] = function (state, action) 66 | return assign(initState, state, { 67 | name = action.name 68 | }) 69 | end, 70 | 71 | ["PROFILE_UPDATE_AGE"] = function (state, action) 72 | return assign(initState, state, { 73 | age = action.age 74 | }) 75 | end, 76 | 77 | ["PROFILE_REMOVE"] = function (state, action) 78 | return Null 79 | end, 80 | } 81 | 82 | return function (state, action) 83 | state = state or Null 84 | local handler = handlers[action.type] 85 | if handler then 86 | return handler(state, action) 87 | end 88 | return state 89 | end 90 | ``` 91 | 92 | ### Combine reducers 93 | ```lua 94 | --[[ 95 | reducers/index.lua 96 | --]] 97 | local combineReducers = require 'redux.combineReducers' 98 | local profile = require 'reducers.profile' 99 | 100 | return combineReducers({ 101 | profile = profile 102 | }) 103 | ``` 104 | 105 | ### Create store 106 | ```lua 107 | --[[ 108 | store.lua 109 | --]] 110 | local createStore = require 'redux.createStore' 111 | local reducers = require 'reducers.index' 112 | 113 | local store = createStore(reducers) 114 | 115 | return store 116 | ``` 117 | 118 | ### Create store with middlewares 119 | Here is an example about how to define a middleware. 120 | ```lua 121 | --[[ 122 | middlewares/logger.lua 123 | --]] 124 | local Logger = require 'redux.utils.logger' 125 | 126 | local function logger(store) 127 | return function (nextDispatch) 128 | return function (action) 129 | Logger.info('WILL DISPATCH:', action) 130 | local ret = nextDispatch(action) 131 | Logger.info('STATE AFTER DISPATCH:', store.getState()) 132 | return ret 133 | 134 | end 135 | end 136 | end 137 | 138 | return logger 139 | ``` 140 | 141 | Compose all defined middlewares to `middlewares` array. 142 | ```lua 143 | --[[ 144 | middlewares/index.lua 145 | --]] 146 | local logger = require 'middlewares.logger' 147 | local thunk = require 'middlewares.thunk' 148 | 149 | local middlewares = { 150 | thunk, 151 | logger, 152 | } 153 | 154 | return middlewares 155 | ``` 156 | 157 | Finally, pass middlewares to `applyMiddleware`, which is provided as an enhancer to `createStore`, and create our store instance. 158 | ```lua 159 | --[[ 160 | store.lua 161 | --]] 162 | local createStore = require 'redux.createStore' 163 | local reducers = require 'reducers.index' 164 | local applyMiddleware = require 'redux.applyMiddleware' 165 | local middlewares = require 'middlewares.index' 166 | 167 | local store = createStore(reducers, applyMiddleware(table.unpack(middlewares))) 168 | 169 | return store 170 | ``` 171 | ### Dispatch & Subscription 172 | ```lua 173 | --[[ 174 | main.lua 175 | --]] 176 | local ProfileActions = require 'actions.profile' 177 | local inspect = require 'redux.helpers.inspect' 178 | local store = require 'store' 179 | 180 | local function callback() 181 | print(inspect(store.getState())) 182 | end 183 | 184 | -- subscribe dispatching 185 | local unsubscribe = store.subscribe(callback) 186 | 187 | -- dispatch actions 188 | store.dispatch(ProfileActions.updateName('Jack')) 189 | store.dispatch(ProfileActions.updateAge(10)) 190 | store.dispatch(ProfileActions.thunkCall()) 191 | 192 | -- unsubscribe 193 | unsubscribe() 194 | ``` 195 | 196 | ### Debug mode 197 | redux-lua is on `Debug` mode by default. Messages with errors and warnings will be output when `Debug` mode is on. Use following code to turn it off. 198 | ```lua 199 | local Env = require 'redux.env' 200 | 201 | Env.setDebug(false) 202 | ``` 203 | 204 | ### Null vs. nil 205 | `nil` is not allowed as a reducer result. If you want any reducer to hold no value, you can return `Null` instead of `nil`. 206 | ```lua 207 | local Null = require 'redux.null' 208 | ``` 209 | 210 | 211 | ## License 212 | [MIT License](https://github.com/pyericz/redux-lua/blob/master/LICENSE) 213 | -------------------------------------------------------------------------------- /examples/actions/profile.lua: -------------------------------------------------------------------------------- 1 | local actions = {} 2 | 3 | function actions.updateName(name) 4 | return { 5 | type = "PROFILE_UPDATE_NAME", 6 | name = name 7 | } 8 | end 9 | 10 | function actions.updateAge(age) 11 | return { 12 | type = "PROFILE_UPDATE_AGE", 13 | age = age 14 | } 15 | end 16 | 17 | function actions.done() 18 | return { 19 | type = "PROFILE_DONE", 20 | } 21 | end 22 | 23 | 24 | function actions.thunkCall() 25 | return function (dispatch, state) 26 | return dispatch(actions.updateAge(3)) 27 | end 28 | end 29 | 30 | return actions 31 | -------------------------------------------------------------------------------- /examples/main.lua: -------------------------------------------------------------------------------- 1 | local ProfileActions = require 'examples.actions.profile' 2 | local inspect = require 'inspect' 3 | local store = require 'examples.store' 4 | 5 | local function callback() 6 | print(inspect(store.getState())) 7 | end 8 | 9 | -- subscribe dispatching 10 | local unsubscribe = store.subscribe(callback) 11 | 12 | -- dispatch actions 13 | store.dispatch(ProfileActions.updateName('Jack')) 14 | store.dispatch(ProfileActions.updateAge(10)) 15 | store.dispatch(ProfileActions.thunkCall()) 16 | 17 | -- unsubscribe 18 | unsubscribe() 19 | -------------------------------------------------------------------------------- /examples/middlewares/index.lua: -------------------------------------------------------------------------------- 1 | local logger = require 'examples.middlewares.logger' 2 | local thunk = require 'examples.middlewares.thunk' 3 | 4 | local middlewares = { 5 | thunk, 6 | logger, 7 | } 8 | 9 | return middlewares 10 | -------------------------------------------------------------------------------- /examples/middlewares/logger.lua: -------------------------------------------------------------------------------- 1 | local Logger = require 'src.utils.logger' 2 | 3 | local function logger(store) 4 | return function (nextDispatch) 5 | return function (action) 6 | Logger.info('WILL DISPATCH:', action) 7 | local ret = nextDispatch(action) 8 | Logger.info('STATE AFTER DISPATCH:', store.getState()) 9 | return ret 10 | 11 | end 12 | end 13 | end 14 | 15 | return logger 16 | -------------------------------------------------------------------------------- /examples/middlewares/thunk.lua: -------------------------------------------------------------------------------- 1 | local function thunk(store) 2 | return function (nextDispatch) 3 | return function (action) 4 | if type(action) == 'function' then 5 | return action(store.dispatch, store.getState) 6 | end 7 | return nextDispatch(action) 8 | end 9 | end 10 | end 11 | 12 | return thunk 13 | -------------------------------------------------------------------------------- /examples/reducers/index.lua: -------------------------------------------------------------------------------- 1 | local combineReducers = require 'src.combineReducers' 2 | local profile = require 'examples.reducers.profile' 3 | 4 | return combineReducers({ 5 | profile = profile 6 | }) 7 | -------------------------------------------------------------------------------- /examples/reducers/profile.lua: -------------------------------------------------------------------------------- 1 | local assign = require 'src.helpers.assign' 2 | local Null = require 'src.null' 3 | 4 | local initState = { 5 | name = '', 6 | age = 0 7 | } 8 | 9 | local handlers = { 10 | ["PROFILE_UPDATE_NAME"] = function (state, action) 11 | return assign(initState, state, { 12 | name = action.name 13 | }) 14 | end, 15 | ["PROFILE_UPDATE_AGE"] = function (state, action) 16 | return assign(initState, state, { 17 | age = action.age 18 | }) 19 | end, 20 | ["PROFILE_DONE"] = function (state, action) 21 | return Null 22 | end 23 | } 24 | 25 | return function (state, action) 26 | state = state or Null 27 | local handler = handlers[action.type] 28 | if handler then 29 | return handler(state, action) 30 | end 31 | return state 32 | end 33 | -------------------------------------------------------------------------------- /examples/store.lua: -------------------------------------------------------------------------------- 1 | local createStore = require 'src.createStore' 2 | local reducers = require 'examples.reducers.index' 3 | local applyMiddleware = require 'src.applyMiddleware' 4 | local middlewares = require 'examples.middlewares.index' 5 | 6 | local unpack = unpack or table.unpack 7 | 8 | local store = createStore(reducers, applyMiddleware(unpack(middlewares))) 9 | 10 | return store 11 | -------------------------------------------------------------------------------- /rockspecs/lredux-0.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lredux" 2 | version = "0.1.0-1" 3 | source = { 4 | url = "git+https://github.com/pyericz/redux-lua", 5 | tag = "v0.1.0" 6 | } 7 | description = { 8 | summary = "A redux implementation using Lua language.", 9 | homepage = "https://github.com/pyericz/redux-lua", 10 | license = "MIT " 11 | } 12 | build = { 13 | type = "builtin", 14 | modules = { 15 | ["lredux.createStore"] = "src/createStore.lua", 16 | ["lredux.applyMiddleware"] = "src/applyMiddleware.lua", 17 | ["lredux.combineReducers"] = "src/combineReducers.lua", 18 | ["lredux.compose"] = "src/compose.lua", 19 | ["lredux.null"] = "src/null.lua", 20 | ["lredux.object"] = "src/object.lua", 21 | ["lredux.env"] = "src/env.lua", 22 | ["lredux.helpers.array"] = "src/helpers/array.lua", 23 | ["lredux.helpers.table"] = "src/helpers/table.lua", 24 | ["lredux.utils.actionTypes"] = "src/utils/actionTypes.lua", 25 | ["lredux.utils.isPlainObject"] = "src/utils/isPlainObject.lua", 26 | ["lredux.utils.logger"] = "src/utils/logger.lua", 27 | } 28 | } 29 | dependencies = { 30 | "lua >= 5.1", 31 | "inspect >= 3.1.1" 32 | } 33 | -------------------------------------------------------------------------------- /rockspecs/lredux-0.2.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lredux" 2 | version = "0.2.0-1" 3 | source = { 4 | url = "git+https://github.com/pyericz/redux-lua", 5 | tag = "v0.2.0" 6 | } 7 | description = { 8 | summary = "Implement redux using Lua language.", 9 | detailed = "With lredux, all the redux features are available on your Lua projects. Try it out! :-D", 10 | homepage = "https://github.com/pyericz/redux-lua", 11 | license = "MIT " 12 | } 13 | build = { 14 | type = "builtin", 15 | modules = { 16 | ["lredux.createStore"] = "lredux/createStore.lua", 17 | ["lredux.applyMiddleware"] = "lredux/applyMiddleware.lua", 18 | ["lredux.combineReducers"] = "lredux/combineReducers.lua", 19 | ["lredux.compose"] = "lredux/compose.lua", 20 | ["lredux.null"] = "lredux/null.lua", 21 | ["lredux.object"] = "lredux/object.lua", 22 | ["lredux.env"] = "lredux/env.lua", 23 | ["lredux.helpers.array"] = "lredux/helpers/array.lua", 24 | ["lredux.helpers.table"] = "lredux/helpers/table.lua", 25 | ["lredux.utils.actionTypes"] = "lredux/utils/actionTypes.lua", 26 | ["lredux.utils.isPlainObject"] = "lredux/utils/isPlainObject.lua", 27 | ["lredux.utils.logger"] = "lredux/utils/logger.lua", 28 | } 29 | } 30 | dependencies = { 31 | "lua >= 5.1", 32 | "inspect >= 3.1.1" 33 | } 34 | -------------------------------------------------------------------------------- /rockspecs/lredux-0.3.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lredux" 2 | version = "0.3.0-1" 3 | source = { 4 | url = "git+https://github.com/pyericz/redux-lua", 5 | tag = "v0.3.0" 6 | } 7 | description = { 8 | summary = "Implement redux using Lua language.", 9 | detailed = "With lredux, all the redux features are available on your Lua projects. Try it out! :-D", 10 | homepage = "https://github.com/pyericz/redux-lua", 11 | license = "MIT " 12 | } 13 | build = { 14 | type = "builtin", 15 | modules = { 16 | ["lredux.createStore"] = "lredux/createStore.lua", 17 | ["lredux.applyMiddleware"] = "lredux/applyMiddleware.lua", 18 | ["lredux.combineReducers"] = "lredux/combineReducers.lua", 19 | ["lredux.compose"] = "lredux/compose.lua", 20 | ["lredux.null"] = "lredux/null.lua", 21 | ["lredux.env"] = "lredux/env.lua", 22 | ["lredux.helpers.array"] = "lredux/helpers/array.lua", 23 | ["lredux.helpers.assign"] = "lredux/helpers/assign.lua", 24 | ["lredux.utils.actionTypes"] = "lredux/utils/actionTypes.lua", 25 | ["lredux.utils.isPlainObject"] = "lredux/utils/isPlainObject.lua", 26 | ["lredux.utils.logger"] = "lredux/utils/logger.lua", 27 | ["lredux.utils.inspect"] = "lredux/utils/inspect.lua", 28 | } 29 | } 30 | dependencies = { 31 | "lua >= 5.1" 32 | } 33 | -------------------------------------------------------------------------------- /rockspecs/redux-lua-0.1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "redux-lua" 2 | version = "0.1.0-1" 3 | source = { 4 | url = "git+https://github.com/pyericz/redux-lua", 5 | tag = "v0.4.0" 6 | } 7 | description = { 8 | summary = "Implement redux using Lua language.", 9 | detailed = "With redux-lua, all the redux features are available on your Lua projects. Try it out! :-D", 10 | homepage = "https://github.com/pyericz/redux-lua", 11 | license = "MIT " 12 | } 13 | build = { 14 | type = "builtin", 15 | modules = { 16 | ["redux.createStore"] = "src/createStore.lua", 17 | ["redux.applyMiddleware"] = "src/applyMiddleware.lua", 18 | ["redux.combineReducers"] = "src/combineReducers.lua", 19 | ["redux.compose"] = "src/compose.lua", 20 | ["redux.null"] = "src/null.lua", 21 | ["redux.env"] = "src/env.lua", 22 | ["redux.helpers.array"] = "src/helpers/array.lua", 23 | ["redux.helpers.assign"] = "src/helpers/assign.lua", 24 | ["redux.utils.actionTypes"] = "src/utils/actionTypes.lua", 25 | ["redux.utils.isPlainObject"] = "src/utils/isPlainObject.lua", 26 | ["redux.utils.logger"] = "src/utils/logger.lua", 27 | ["redux.utils.inspect"] = "src/utils/inspect.lua", 28 | } 29 | } 30 | dependencies = { 31 | "lua >= 5.1" 32 | } 33 | -------------------------------------------------------------------------------- /spec/lredux_spec.lua: -------------------------------------------------------------------------------- 1 | local ProfileActions = require 'examples.actions.profile' 2 | local store = require 'examples.store' 3 | 4 | describe('lredux', function () 5 | describe('state', function () 6 | it('init state', function () 7 | assert.equals(next(store.getState()), nil) 8 | end) 9 | it('dispatch', function () 10 | store.dispatch(ProfileActions.updateName('Jack')) 11 | assert.equals(store.getState().profile.name, 'Jack') 12 | 13 | store.dispatch(ProfileActions.updateAge(10)) 14 | assert.equals(store.getState().profile.age, 10) 15 | 16 | store.dispatch(ProfileActions.done()) 17 | assert.equals(next(store.getState()), nil) 18 | end) 19 | end) 20 | end) 21 | -------------------------------------------------------------------------------- /src/applyMiddleware.lua: -------------------------------------------------------------------------------- 1 | local directory = (...):match("(.-)[^%.]+$") 2 | local Array = require(directory .. 'helpers.array') 3 | local assign = require(directory .. 'helpers.assign') 4 | local compose = require(directory .. 'compose') 5 | 6 | local unpack = unpack or table.unpack 7 | --[[ 8 | Creates a store enhancer that applies middleware to the dispatch method 9 | of the Redux store. This is handy for a variety of tasks, such as expressing 10 | asynchronous actions in a concise manner, or logging every action payload. 11 | 12 | See `redux-thunk` package as an example of the Redux middleware. 13 | 14 | Because middleware is potentially asynchronous, this should be the first 15 | store enhancer in the composition chain. 16 | 17 | Note that each middleware will be given the `dispatch` and `getState` functions 18 | as named arguments. 19 | 20 | @param {...Function} middlewares The middleware chain to be applied. 21 | @returns {Function} A store enhancer applying the middleware. 22 | --]] 23 | local function applyMiddleware(...) 24 | local middlewares = {...} 25 | return function (createStore) 26 | return function (...) 27 | local store = createStore(...) 28 | local function dispatch() 29 | error(table.concat{ 30 | [[Dispatching while constructing your middleware is not allowed. ]], 31 | [[Other middleware would not be applied to this dispatch.]] 32 | }) 33 | end 34 | 35 | -- Expose only a subset of the store API to the middlewares: 36 | -- `dispatch(action)` and `getState()` 37 | local middlewareAPI = { 38 | getState = store.getState, 39 | dispatch = function (...) 40 | return dispatch(...) 41 | end 42 | } 43 | local chain = Array.map( 44 | middlewares, 45 | function(middleware) 46 | return middleware(middlewareAPI) 47 | end 48 | ) 49 | dispatch = compose(unpack(chain))(store.dispatch) 50 | 51 | return assign({}, store, { 52 | dispatch = dispatch 53 | }) 54 | end 55 | end 56 | end 57 | 58 | return applyMiddleware 59 | -------------------------------------------------------------------------------- /src/combineReducers.lua: -------------------------------------------------------------------------------- 1 | local directory = (...):match("(.-)[^%.]+$") 2 | local Logger = require(directory..'utils.logger') 3 | local Env = require(directory..'env') 4 | local inspect = require(directory..'utils.inspect') 5 | local ActionTypes = require(directory..'utils.actionTypes') 6 | local isPlainObject = require(directory..'utils.isPlainObject') 7 | local Null = require(directory..'null') 8 | 9 | local concat = table.concat 10 | 11 | local function keys(tbl) 12 | assert(type(tbl) == 'table', 'expected a table value.') 13 | local ret = {} 14 | for k, _ in pairs(tbl) do 15 | table.insert(ret, k) 16 | end 17 | return ret 18 | end 19 | 20 | local function getNilStateErrorMessage(key, action) 21 | local actionType = action and action.type or nil 22 | local actionDesc = actionType and string.format([[action "%s"]], actionType) or 'an action' 23 | 24 | return string.format(concat{ 25 | [[Given %s, reducer "%s" returned nil. ]], 26 | [[To ignore an action, you must explicitly return the previous state. ]], 27 | [[If you want this reducer to hold no value, you can return Null instead of nil.]] 28 | }, actionDesc, key) 29 | end 30 | 31 | local function getUnexpectedStateShapeWarningMessage( 32 | inputState, 33 | reducers, 34 | action, 35 | unexpectedKeyCache 36 | ) 37 | 38 | local reducerKeys = keys(reducers) 39 | local argumentName = 'previous state received by the reducer' 40 | if action and action.type == ActionTypes.INIT then 41 | argumentName = 'preloadedState argument passed to createStore' 42 | end 43 | 44 | if #reducerKeys == 0 then 45 | return concat{ 46 | "Store does not have a valid reducer. Make sure the argument passed ", 47 | "to combineReducers is an object whose values are reducers." 48 | } 49 | end 50 | 51 | if not isPlainObject(inputState) then 52 | return string.format(concat{ 53 | [[The %s has unexpected type of "%s". ]], 54 | [[Expected argument to be an object with the following keys: "%s".]] 55 | }, 56 | argumentName, 57 | inspect(inputState, {depth = 1}), 58 | table.concat(reducerKeys, '", "') 59 | ) 60 | end 61 | 62 | local unexpectedKeys = {} 63 | for key, _ in pairs(inputState) do 64 | if reducers[key] == nil and unexpectedKeyCache[key] == nil then 65 | table.insert(unexpectedKeys, key) 66 | end 67 | end 68 | 69 | for _, key in ipairs(unexpectedKeys) do 70 | unexpectedKeyCache[key] = true 71 | end 72 | 73 | if type(action) == 'table' and action.type == ActionTypes.REPLACE then return end 74 | 75 | if #unexpectedKeys > 0 then 76 | return string.format(concat{ 77 | [[Unexpected %s "%s" found in %s. ]], 78 | [[Expected to find one of the known reducer keys instead: ]], 79 | [["%s". Unexpected keys will be ignored.]] 80 | }, 81 | #unexpectedKeys > 1 and 'keys' or 'key', 82 | table.concat(unexpectedKeys, '", "'), 83 | argumentName, 84 | table.concat(reducerKeys, '", "') 85 | ) 86 | end 87 | end 88 | 89 | local function assertReducerShape(reducers) 90 | for key, reducer in pairs(reducers) do 91 | local initState = reducer(nil, { type = ActionTypes.INIT }) 92 | 93 | if initState == nil then 94 | error(string.format(concat{ 95 | [[Reducer "%s" returned nil during initialization. ]], 96 | [[If the state passed to the reducer is nil, you must ]], 97 | [[explicitly return the initial state. The initial state may ]], 98 | [[not be nil. If you don't want to set a value for this reducer, ]], 99 | [[you can use Null instead of nil.]] 100 | }, key)) 101 | end 102 | 103 | local state = reducer(nil, { type = ActionTypes.PROBE_UNKNOWN_ACTION() }) 104 | if state == nil then 105 | error(string.format(concat{ 106 | [[Reducer "%s" returned nil when probed with a random type. ]], 107 | [[Don't try to handle %s or other actions in "redux/*" ]], 108 | [[namespace. They are considered private. Instead, you must return the ]], 109 | [[current state for any unknown actions, unless it is nil, ]], 110 | [[in which case you must return the initial state, regardless of the ]], 111 | [[action type. The initial state may not be nil, but can be Null. ]] 112 | }, key, ActionTypes.INIT)) 113 | end 114 | end 115 | end 116 | 117 | --[[ 118 | Turns an object whose values are different reducer functions, into a single 119 | reducer function. It will call every child reducer, and gather their results 120 | into a single state object, whose keys correspond to the keys of the passed 121 | reducer functions. 122 | 123 | @param {table} reducers An object whose values correspond to different 124 | reducer functions that need to be combined into one. One handy way to obtain 125 | it is to use ES6 `import * as reducers` syntax. The reducers may never return 126 | undefined for any action. Instead, they should return their initial state 127 | if the state passed to them was undefined, and the current state for any 128 | unrecognized action. 129 | 130 | @returns {Function} A reducer function that invokes every reducer inside the 131 | passed object, and builds a state object with the same shape. 132 | --]] 133 | local function combineReducers(reducers) 134 | local finalReducers = {} 135 | for k,v in pairs(reducers) do 136 | if type(v) ~= 'function' then 137 | Logger.warn(string.format('Reducer type `%s` is not supported for key `%s`.', type(v), k)) 138 | else 139 | finalReducers[k] = v 140 | end 141 | end 142 | 143 | local finalReducerKeys = keys(finalReducers) 144 | 145 | -- This is used to make sure we don't warn about the same 146 | -- keys multiple times. 147 | local unexpectedKeyCache 148 | if Env.isDebug() then 149 | unexpectedKeyCache = {} 150 | end 151 | 152 | local _, shapeAssertionError = pcall(assertReducerShape, finalReducers) 153 | 154 | return function(state, action) 155 | if state == nil then state = {} end 156 | if shapeAssertionError then 157 | Logger.error(shapeAssertionError) 158 | return 159 | end 160 | 161 | if Env.isDebug() then 162 | local warningMessage = getUnexpectedStateShapeWarningMessage( 163 | state, 164 | finalReducers, 165 | action, 166 | unexpectedKeyCache 167 | ) 168 | if warningMessage then 169 | Logger.warn(warningMessage) 170 | end 171 | end 172 | 173 | local hasChanged = false 174 | local nextState = {} 175 | for i=1, #finalReducerKeys do 176 | local key = finalReducerKeys[i] 177 | local reducer = finalReducers[key] 178 | local previousStateForKey = state[key] 179 | local nextStateForKey = reducer(previousStateForKey, action) 180 | if nextStateForKey == nil then 181 | local errorMessage = getNilStateErrorMessage(key, action) 182 | error(errorMessage) 183 | end 184 | if nextStateForKey == Null then 185 | nextStateForKey = nil 186 | end 187 | nextState[key] = nextStateForKey 188 | hasChanged = hasChanged or nextStateForKey ~= previousStateForKey 189 | end 190 | return hasChanged and nextState or state 191 | end 192 | end 193 | 194 | return combineReducers 195 | -------------------------------------------------------------------------------- /src/compose.lua: -------------------------------------------------------------------------------- 1 | local directory = (...):match("(.-)[^%.]+$") 2 | local Array = require(directory..'helpers.array') 3 | 4 | --[[ 5 | Composes single-argument functions from right to left. The rightmost 6 | function can take multiple arguments as it provides the signature for 7 | the resulting composite function. 8 | 9 | @param {...Function} funcs The functions to compose. 10 | @returns {Function} A function obtained by composing the argument functions 11 | from right to left. For example, compose(f, g, h) is identical to doing 12 | (...args) => f(g(h(...args))). 13 | --]] 14 | 15 | local function compose(...) 16 | local funcs = {...} 17 | if #funcs == 0 then 18 | return function(argument) 19 | return argument 20 | end 21 | end 22 | 23 | if #funcs == 1 then 24 | return funcs[1] 25 | end 26 | 27 | return Array.reduce( 28 | funcs, 29 | function(a, b) 30 | return function (...) 31 | return a(b(...)) 32 | end 33 | end 34 | ) 35 | end 36 | 37 | return compose 38 | -------------------------------------------------------------------------------- /src/createStore.lua: -------------------------------------------------------------------------------- 1 | local directory = (...):match("(.-)[^%.]+$") 2 | local Logger = require(directory..'utils.logger') 3 | local assign = require(directory..'helpers.assign') 4 | local Array = require(directory..'helpers.array') 5 | local ActionTypes = require(directory..'utils.actionTypes') 6 | local isPlainObject = require(directory..'utils.isPlainObject') 7 | 8 | local concat = table.concat 9 | 10 | --[[ 11 | Creates a Redux store that holds the state tree. 12 | The only way to change the data in the store is to call `dispatch()` on it. 13 | 14 | There should only be a single store in your app. To specify how different 15 | parts of the state tree respond to actions, you may combine several reducers 16 | into a single reducer function by using `combineReducers`. 17 | 18 | @param {function} reducer A function that returns the next state tree, given 19 | the current state tree and the action to handle. 20 | 21 | @param {any} [preloadedState] The initial state. You may optionally specify it 22 | to hydrate the state from the server in universal apps, or to restore a 23 | previously serialized user session. 24 | If you use `combineReducers` to produce the root reducer function, this must be 25 | an object with the same shape as `combineReducers` keys. 26 | 27 | @param {function} [enhancer] The store enhancer. You may optionally specify it 28 | to enhance the store with third-party capabilities such as middleware, 29 | time travel, persistence, etc. The only store enhancer that ships with Redux 30 | is `applyMiddleware()`. 31 | 32 | @returns {store} A Redux store that lets you read the state, dispatch actions 33 | and subscribe to changes. 34 | --]] 35 | local function createStore(reducer, preloadState, enhancer) 36 | if (type(preloadState) == 'function' and type(enhancer) == 'function') or 37 | (type(enhancer) == 'function' and type(arg[4]) == 'function') then 38 | error(concat{ 39 | [[It looks like you are passing several store enhancers to ]], 40 | [[createStore(). This is not supported. Instead, compose them ]], 41 | [[together to a single function.]] 42 | }) 43 | end 44 | 45 | if (type(preloadState) == 'function' and type(enhancer) == 'nil') then 46 | enhancer = preloadState 47 | preloadState = nil 48 | end 49 | 50 | if enhancer ~= nil then 51 | assert(type(enhancer) == 'function','Expected the enhancer to be a function.') 52 | return enhancer(createStore)(reducer, preloadState) 53 | end 54 | 55 | assert(type(reducer) == 'function', 'Expected the reducer to be a function.') 56 | 57 | local currentReducer = reducer 58 | local currentState = preloadState 59 | local currentListeners = {} 60 | local nextListeners = currentListeners 61 | local isDispatching = false 62 | 63 | --[[ 64 | This makes a shallow copy of currentListeners so we can use 65 | nextListeners as a temporary list while dispatching. 66 | 67 | This prevents any bugs around consumers calling 68 | subscribe/unsubscribe in the middle of a dispatch. 69 | --]] 70 | local function ensureCanMutateNextListeners() 71 | if nextListeners == currentListeners then 72 | nextListeners = assign({}, currentListeners) 73 | end 74 | end 75 | 76 | 77 | --[[ 78 | Reads the state tree managed by the store. 79 | @returns {any} The current state tree of your application. 80 | --]] 81 | local function getState() 82 | if isDispatching then 83 | error(concat{ 84 | [[You may not call store.getState() while the reducer is executing. ]], 85 | [[The reducer has already received the state as an argument. ]], 86 | [[Pass it down from the top reducer instead of reading it from the store.]] 87 | }) 88 | end 89 | return currentState 90 | end 91 | 92 | 93 | --[[ 94 | Adds a change listener. It will be called any time an action is dispatched, 95 | and some part of the state tree may potentially have changed. You may then 96 | call `getState()` to read the current state tree inside the callback. 97 | 98 | You may call `dispatch()` from a change listener, with the following 99 | caveats: 100 | 101 | 1. The subscriptions are snapshotted just before every `dispatch()` call. 102 | If you subscribe or unsubscribe while the listeners are being invoked, this 103 | will not have any effect on the `dispatch()` that is currently in progress. 104 | However, the next `dispatch()` call, whether nested or not, will use a more 105 | recent snapshot of the subscription list. 106 | 107 | 2. The listener should not expect to see all state changes, as the state 108 | might have been updated multiple times during a nested `dispatch()` before 109 | the listener is called. It is, however, guaranteed that all subscribers 110 | registered before the `dispatch()` started will be called with the latest 111 | state by the time it exits. 112 | 113 | @param {function} listener A callback to be invoked on every dispatch. 114 | @returns {function} A function to remove this change listener. 115 | --]] 116 | local function subscribe(listener) 117 | assert(type(listener) == 'function', 'Expected the listener to be a function.') 118 | 119 | if isDispatching then 120 | error(concat{ 121 | [[You may not call store.subscribe() while the reducer is executing. ]], 122 | [[If you would like to be notified after the store has been updated, subscribe from a ]], 123 | [[component and invoke store.getState() in the callback to access the latest state. ]], 124 | [[See https://redux.js.org/api-reference/store#subscribe(listener) for more details.]] 125 | }) 126 | end 127 | 128 | local isSubscribed = true 129 | ensureCanMutateNextListeners() 130 | table.insert(nextListeners, listener) 131 | 132 | return function() 133 | if not isSubscribed then 134 | return 135 | end 136 | if isDispatching then 137 | error(table.concat{ 138 | [[You may not unsubscribe from a store listener while the reducer is executing. ]], 139 | [[See https://redux.js.org/api-reference/store#subscribe(listener) for more details.]] 140 | }) 141 | end 142 | isSubscribed = false 143 | ensureCanMutateNextListeners() 144 | local index = Array.indexOf(nextListeners, listener) 145 | if index then 146 | table.remove(nextListeners, index) 147 | end 148 | end 149 | end 150 | 151 | --[[ 152 | Dispatches an action. It is the only way to trigger a state change. 153 | 154 | The `reducer` function, used to create the store, will be called with the 155 | current state tree and the given `action`. Its return value will 156 | be considered the **next** state of the tree, and the change listeners 157 | will be notified. 158 | 159 | The base implementation only supports table actions. If you want to 160 | dispatch something else, you need to 161 | wrap your store creating function into the corresponding middleware. Even the 162 | middleware will eventually dispatch table actions using this method. 163 | 164 | @param {table} action A table representing “what changed”. It is 165 | a good idea to keep actions serializable so you can record and replay user 166 | sessions. An action must have 167 | a `type` property which may not be `nil`. It is a good idea to use 168 | string constants for action types. 169 | 170 | @returns {table} For convenience, the same action table you dispatched. 171 | 172 | Note that, if you use a custom middleware, it may wrap `dispatch()` to 173 | return something else. 174 | --]] 175 | local function dispatch(action) 176 | if not isPlainObject(action) then 177 | error(concat{ 178 | [[Actions must be plain objects. ]], 179 | [[Use custom middleware for async actions.]] 180 | }) 181 | end 182 | assert(type(action.type) ~= 'nil', "Actions may not have an nil 'type' key. Have you misspelled a constant?") 183 | 184 | if isDispatching then 185 | error('Reducers may not dispatch actions.') 186 | end 187 | 188 | isDispatching = true 189 | local status, err = pcall(currentReducer, currentState, action) 190 | if status then 191 | currentState = err 192 | else 193 | Logger.error(err) 194 | end 195 | isDispatching = false 196 | 197 | currentListeners = nextListeners 198 | for i=1,#currentListeners do 199 | local listener = currentListeners[i] 200 | listener() 201 | end 202 | return action 203 | end 204 | 205 | --[[ 206 | Replaces the reducer currently used by the store to calculate the state. 207 | 208 | You might need this if your app implements code splitting and you want to 209 | load some of the reducers dynamically. You might also need this if you 210 | implement a hot reloading mechanism for Redux. 211 | 212 | @param {function} nextReducer The reducer for the store to use instead. 213 | @returns {nil} 214 | --]] 215 | local function replaceReducer(nextReducer) 216 | assert(type(nextReducer) == 'function', 'Expected the nextReducer to be a function.') 217 | 218 | currentReducer = nextReducer 219 | 220 | -- This action has a similiar effect to ActionTypes.INIT. 221 | -- Any reducers that existed in both the new and old rootReducer 222 | -- will receive the previous state. This effectively populates 223 | -- the new state tree with any relevant data from the old one. 224 | dispatch( {type = ActionTypes.REPLACE} ) 225 | end 226 | 227 | local function observable() 228 | local outerSubscribe = subscribe 229 | return { 230 | subscribe = function (observer) 231 | assert(type(observer) == 'table', "Expected the observer to be an table.") 232 | local function observeState() 233 | if (observer.next) then 234 | observer.next(getState()) 235 | end 236 | end 237 | observeState() 238 | return { 239 | unsubscribe = outerSubscribe(observeState) 240 | } 241 | end 242 | } 243 | end 244 | 245 | -- When a store is created, an "INIT" action is dispatched so that every 246 | -- reducer returns their initial state. This effectively populates 247 | -- the initial state tree. 248 | dispatch( {type = ActionTypes.INIT} ) 249 | 250 | return { 251 | dispatch = dispatch, 252 | subscribe = subscribe, 253 | getState = getState, 254 | replaceReducer = replaceReducer, 255 | observable = observable 256 | } 257 | end 258 | 259 | return createStore 260 | -------------------------------------------------------------------------------- /src/env.lua: -------------------------------------------------------------------------------- 1 | local Env = {} 2 | 3 | local __DEBUG__ = true 4 | 5 | function Env.setDebug(flag) 6 | __DEBUG__ = flag 7 | end 8 | 9 | function Env.isDebug() 10 | return __DEBUG__ 11 | end 12 | 13 | return Env 14 | -------------------------------------------------------------------------------- /src/helpers/array.lua: -------------------------------------------------------------------------------- 1 | -- map(table, function) 2 | -- e.g: map({1, 2, 3}, double) -> {2, 4, 6} 3 | local function map(tbl, func) 4 | local ret = {} 5 | for i,v in ipairs(tbl) do 6 | ret[i] = func(v) 7 | end 8 | return ret 9 | end 10 | 11 | -- filter(table, function) 12 | -- e.g: filter({1, 2, 3, 4}, is_even) -> {2, 4} 13 | local function filter(tbl, func) 14 | local ret = {} 15 | for i,v in ipairs(tbl) do 16 | if func(v) then 17 | ret[i] = v 18 | end 19 | end 20 | return ret 21 | end 22 | 23 | -- head(table) 24 | -- e.g: head({1, 2, 3}) -> 1 25 | local function head(tbl) 26 | return tbl[1] 27 | end 28 | 29 | -- tail(table) 30 | -- e.g: tail({1, 2, 3}) -> {2, 3} 31 | local function tail(tbl) 32 | if #tbl < 1 then 33 | return nil 34 | else 35 | local ret = {} 36 | for i=2,#tbl do 37 | table.insert(ret, tbl[i]) 38 | end 39 | return ret 40 | end 41 | end 42 | 43 | -- foldr(function, default, table) 44 | -- e.g: foldr(operator.mul, 1, {1,2,3,4,5}) -> 120 45 | local function foldr(tbl, val, func) 46 | for _,v in pairs(tbl) do 47 | val = func(val, v) 48 | end 49 | return val 50 | end 51 | 52 | -- reduce(table, function) 53 | -- e.g: reduce({1,2,3,4}, operator.add) -> 10 54 | local function reduce(tbl, reducer) 55 | return foldr(tail(tbl), head(tbl), reducer) 56 | end 57 | 58 | local function slice(tbl, i, j) 59 | assert(type(tbl) == 'table', 'expected a table value.') 60 | local ret = {} 61 | if #tbl == 0 then return ret end 62 | if i == nil then 63 | i = 1 64 | end 65 | 66 | if i < 0 then 67 | i = #tbl + 1 + i 68 | end 69 | 70 | if i < 1 then 71 | i = 1 72 | end 73 | 74 | if j == nil then 75 | j = #tbl 76 | end 77 | 78 | if j < 0 then 79 | j = #tbl + 1 + j 80 | end 81 | 82 | for k=i,#tbl do 83 | if k > j then break end 84 | table.insert(ret, tbl[k]) 85 | end 86 | 87 | return ret 88 | end 89 | 90 | local function indexOf(tbl, value) 91 | assert(type(tbl) == 'table', 'expected a table value.') 92 | for i, v in ipairs(tbl) do 93 | if v == value then 94 | return i 95 | end 96 | end 97 | return nil 98 | end 99 | 100 | 101 | return { 102 | map = map, 103 | filter = filter, 104 | head = head, 105 | tail = tail, 106 | reduce = reduce, 107 | slice = slice, 108 | indexOf = indexOf 109 | } 110 | -------------------------------------------------------------------------------- /src/helpers/assign.lua: -------------------------------------------------------------------------------- 1 | return function (target, ...) 2 | local args = {...} 3 | for i=1, #args do 4 | local tbl = args[i] or {} 5 | for k, v in pairs(tbl) do 6 | target[k] = v 7 | end 8 | end 9 | return target 10 | end 11 | -------------------------------------------------------------------------------- /src/null.lua: -------------------------------------------------------------------------------- 1 | local mt = {} 2 | 3 | function mt.__newindex() 4 | error("attempt to modify a Null value.", 2) 5 | end 6 | 7 | function mt.__index() 8 | error("attempt to index a Null value.", 2) 9 | end 10 | 11 | return setmetatable({}, mt) 12 | -------------------------------------------------------------------------------- /src/utils/actionTypes.lua: -------------------------------------------------------------------------------- 1 | local charset = {} 2 | 3 | -- qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890 4 | for i = 48, 57 do table.insert(charset, string.char(i)) end 5 | for i = 65, 90 do table.insert(charset, string.char(i)) end 6 | for i = 97, 122 do table.insert(charset, string.char(i)) end 7 | 8 | local function randomString(length) 9 | math.randomseed(os.time()) 10 | 11 | if length > 0 then 12 | return randomString(length - 1) .. charset[math.random(1, #charset)] 13 | else 14 | return "" 15 | end 16 | end 17 | 18 | return { 19 | INIT = string.format("@@redux/INIT:%s", randomString(7)), 20 | REPLACE = string.format("@@redux/REPLACE:%s", randomString(7)), 21 | PROBE_UNKNOWN_ACTION = function () 22 | return string.format("@@redux/PROBE_UNKNOWN_ACTION:%s", randomString(7)) 23 | end 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | local function rawpairs(t) 37 | return next, t, nil 38 | end 39 | 40 | -- Apostrophizes the string if it has quotes, but not aphostrophes 41 | -- Otherwise, it returns a regular quoted string 42 | local function smartQuote(str) 43 | if str:match('"') and not str:match("'") then 44 | return "'" .. str .. "'" 45 | end 46 | return '"' .. str:gsub('"', '\\"') .. '"' 47 | end 48 | 49 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 50 | local shortControlCharEscapes = { 51 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 52 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 53 | } 54 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 55 | for i=0, 31 do 56 | local ch = string.char(i) 57 | if not shortControlCharEscapes[ch] then 58 | shortControlCharEscapes[ch] = "\\"..i 59 | longControlCharEscapes[ch] = string.format("\\%03d", i) 60 | end 61 | end 62 | 63 | local function escape(str) 64 | return (str:gsub("\\", "\\\\") 65 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 66 | :gsub("%c", shortControlCharEscapes)) 67 | end 68 | 69 | local function isIdentifier(str) 70 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 71 | end 72 | 73 | local function isSequenceKey(k, sequenceLength) 74 | return type(k) == 'number' 75 | and 1 <= k 76 | and k <= sequenceLength 77 | and math.floor(k) == k 78 | end 79 | 80 | local defaultTypeOrders = { 81 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 82 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 83 | } 84 | 85 | local function sortKeys(a, b) 86 | local ta, tb = type(a), type(b) 87 | 88 | -- strings and numbers are sorted numerically/alphabetically 89 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 90 | 91 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 92 | -- Two default types are compared according to the defaultTypeOrders table 93 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 94 | elseif dta then return true -- default types before custom ones 95 | elseif dtb then return false -- custom types after default ones 96 | end 97 | 98 | -- custom types are sorted out alphabetically 99 | return ta < tb 100 | end 101 | 102 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 103 | -- tables aren't pure sequences. So we implement our own # operator. 104 | local function getSequenceLength(t) 105 | local len = 1 106 | local v = rawget(t,len) 107 | while v ~= nil do 108 | len = len + 1 109 | v = rawget(t,len) 110 | end 111 | return len - 1 112 | end 113 | 114 | local function getNonSequentialKeys(t) 115 | local keys, keysLength = {}, 0 116 | local sequenceLength = getSequenceLength(t) 117 | for k,_ in rawpairs(t) do 118 | if not isSequenceKey(k, sequenceLength) then 119 | keysLength = keysLength + 1 120 | keys[keysLength] = k 121 | end 122 | end 123 | table.sort(keys, sortKeys) 124 | return keys, keysLength, sequenceLength 125 | end 126 | 127 | local function countTableAppearances(t, tableAppearances) 128 | tableAppearances = tableAppearances or {} 129 | 130 | if type(t) == 'table' then 131 | if not tableAppearances[t] then 132 | tableAppearances[t] = 1 133 | for k,v in rawpairs(t) do 134 | countTableAppearances(k, tableAppearances) 135 | countTableAppearances(v, tableAppearances) 136 | end 137 | countTableAppearances(getmetatable(t), tableAppearances) 138 | else 139 | tableAppearances[t] = tableAppearances[t] + 1 140 | end 141 | end 142 | 143 | return tableAppearances 144 | end 145 | 146 | local copySequence = function(s) 147 | local copy, len = {}, #s 148 | for i=1, len do copy[i] = s[i] end 149 | return copy, len 150 | end 151 | 152 | local function makePath(path, ...) 153 | local keys = {...} 154 | local newPath, len = copySequence(path) 155 | for i=1, #keys do 156 | newPath[len + i] = keys[i] 157 | end 158 | return newPath 159 | end 160 | 161 | local function processRecursive(process, item, path, visited) 162 | if item == nil then return nil end 163 | if visited[item] then return visited[item] end 164 | 165 | local processed = process(item, path) 166 | if type(processed) == 'table' then 167 | local processedCopy = {} 168 | visited[item] = processedCopy 169 | local processedKey 170 | 171 | for k,v in rawpairs(processed) do 172 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 173 | if processedKey ~= nil then 174 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 175 | end 176 | end 177 | 178 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 179 | if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field 180 | setmetatable(processedCopy, mt) 181 | processed = processedCopy 182 | end 183 | return processed 184 | end 185 | 186 | 187 | 188 | ------------------------------------------------------------------- 189 | 190 | local Inspector = {} 191 | local Inspector_mt = {__index = Inspector} 192 | 193 | function Inspector:puts(...) 194 | local args = {...} 195 | local buffer = self.buffer 196 | local len = #buffer 197 | for i=1, #args do 198 | len = len + 1 199 | buffer[len] = args[i] 200 | end 201 | end 202 | 203 | function Inspector:down(f) 204 | self.level = self.level + 1 205 | f() 206 | self.level = self.level - 1 207 | end 208 | 209 | function Inspector:tabify() 210 | self:puts(self.newline, string.rep(self.indent, self.level)) 211 | end 212 | 213 | function Inspector:alreadyVisited(v) 214 | return self.ids[v] ~= nil 215 | end 216 | 217 | function Inspector:getId(v) 218 | local id = self.ids[v] 219 | if not id then 220 | local tv = type(v) 221 | id = (self.maxIds[tv] or 0) + 1 222 | self.maxIds[tv] = id 223 | self.ids[v] = id 224 | end 225 | return tostring(id) 226 | end 227 | 228 | function Inspector:putKey(k) 229 | if isIdentifier(k) then return self:puts(k) end 230 | self:puts("[") 231 | self:putValue(k) 232 | self:puts("]") 233 | end 234 | 235 | function Inspector:putTable(t) 236 | if t == inspect.KEY or t == inspect.METATABLE then 237 | self:puts(tostring(t)) 238 | elseif self:alreadyVisited(t) then 239 | self:puts('') 240 | elseif self.level >= self.depth then 241 | self:puts('{...}') 242 | else 243 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 244 | 245 | local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) 246 | local mt = getmetatable(t) 247 | 248 | self:puts('{') 249 | self:down(function() 250 | local count = 0 251 | for i=1, sequenceLength do 252 | if count > 0 then self:puts(',') end 253 | self:puts(' ') 254 | self:putValue(t[i]) 255 | count = count + 1 256 | end 257 | 258 | for i=1, nonSequentialKeysLength do 259 | local k = nonSequentialKeys[i] 260 | if count > 0 then self:puts(',') end 261 | self:tabify() 262 | self:putKey(k) 263 | self:puts(' = ') 264 | self:putValue(t[k]) 265 | count = count + 1 266 | end 267 | 268 | if type(mt) == 'table' then 269 | if count > 0 then self:puts(',') end 270 | self:tabify() 271 | self:puts(' = ') 272 | self:putValue(mt) 273 | end 274 | end) 275 | 276 | if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing } 277 | self:tabify() 278 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 279 | self:puts(' ') 280 | end 281 | 282 | self:puts('}') 283 | end 284 | end 285 | 286 | function Inspector:putValue(v) 287 | local tv = type(v) 288 | 289 | if tv == 'string' then 290 | self:puts(smartQuote(escape(v))) 291 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or 292 | tv == 'cdata' or tv == 'ctype' then 293 | self:puts(tostring(v)) 294 | elseif tv == 'table' then 295 | self:putTable(v) 296 | else 297 | self:puts('<', tv, ' ', self:getId(v), '>') 298 | end 299 | end 300 | 301 | ------------------------------------------------------------------- 302 | 303 | function inspect.inspect(root, options) 304 | options = options or {} 305 | 306 | local depth = options.depth or math.huge 307 | local newline = options.newline or '\n' 308 | local indent = options.indent or ' ' 309 | local process = options.process 310 | 311 | if process then 312 | root = processRecursive(process, root, {}, {}) 313 | end 314 | 315 | local inspector = setmetatable({ 316 | depth = depth, 317 | level = 0, 318 | buffer = {}, 319 | ids = {}, 320 | maxIds = {}, 321 | newline = newline, 322 | indent = indent, 323 | tableAppearances = countTableAppearances(root) 324 | }, Inspector_mt) 325 | 326 | inspector:putValue(root) 327 | 328 | return table.concat(inspector.buffer) 329 | end 330 | 331 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 332 | 333 | return inspect 334 | -------------------------------------------------------------------------------- /src/utils/isPlainObject.lua: -------------------------------------------------------------------------------- 1 | --- 2 | -- @param {any} obj The object to inspect. 3 | -- @returns {boolean} True if the argument appears to be a plain object. 4 | -- 5 | return function (tbl) 6 | if type(tbl) ~= 'table' then 7 | return false 8 | end 9 | return getmetatable(tbl) == nil 10 | end 11 | -------------------------------------------------------------------------------- /src/utils/logger.lua: -------------------------------------------------------------------------------- 1 | local directory = (...):match("(.-)[^%.]+$") 2 | local inspect = require(directory .. 'inspect') 3 | local Logger = {} 4 | 5 | local unpack = unpack or table.unpack 6 | 7 | local print = _G["print"] 8 | 9 | local function prettyPrint(tag, ...) 10 | local msgs = {...} 11 | for i=1, #msgs do 12 | if type(msgs[i]) ~= 'string' then 13 | msgs[i] = inspect(msgs[i]) 14 | end 15 | end 16 | print(tag, unpack(msgs)) 17 | print('') 18 | end 19 | 20 | local function prettyPrintTrace(tag, ...) 21 | local msgs = {...} 22 | for i=1, #msgs do 23 | if type(msgs[i]) ~= 'string' then 24 | msgs[i] = inspect(msgs[i]) 25 | end 26 | end 27 | print(tag, unpack(msgs)) 28 | print(debug.traceback('', 3)) 29 | print('') 30 | end 31 | 32 | function Logger.info(...) 33 | prettyPrint('[INFO]', ...) 34 | end 35 | 36 | function Logger.log(...) 37 | prettyPrint('[LOG]', ...) 38 | end 39 | 40 | function Logger.debug(...) 41 | prettyPrintTrace('[DEBUG]', ...) 42 | end 43 | 44 | function Logger.warn(...) 45 | prettyPrint('[WARN]', ...) 46 | end 47 | 48 | function Logger.error(...) 49 | prettyPrintTrace('[ERROR]', ...) 50 | end 51 | 52 | return Logger 53 | --------------------------------------------------------------------------------