Skip to content

Commit

Permalink
Add Luau types for thunks and the store (#71)
Browse files Browse the repository at this point in the history
Follows up on the types added in: #70.

Adds Luau types for thunks and the store. This is partially inspired by upstream Redux types here and here, but modified to be as useful as possible given Luau's constraints.
  • Loading branch information
jkelaty-rbx authored Jun 13, 2022
1 parent 07f634e commit ce63e3d
Show file tree
Hide file tree
Showing 22 changed files with 142 additions and 18 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ on:
- master

jobs:
analyze:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: install code quality tools
uses: Roblox/setup-foreman@v1
with:
version: "^1.0.1"
token: ${{ secrets.GITHUB_TOKEN }}

- name: Download global Roblox types
shell: bash
run: curl -O https://raw.githubusercontent.com/JohnnyMorganz/luau-analyze-rojo/master/globalTypes.d.lua

- name: Analyze
shell: bash
run: luau-analyze --project=default.project.json --defs=globalTypes.d.lua --defs=testez.d.lua src/


test:
runs-on: ubuntu-latest

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased Changes
* Add makeThunkMiddleware to inject custom argument ([#69](https://github.com/Roblox/rodux/pull/69)).
* Add Luau types for actions and reducers ([#70](https://github.com/Roblox/rodux/pull/70)).
* Add Luau types for thunks and the store ([#71](https://github.com/Roblox/rodux/pull/71)).

## 3.0.0 (2021-03-25)
* Revise error reporting logic; restore default semantics from version 1.x ([#61](https://github.com/Roblox/rodux/pull/61)).
Expand Down
1 change: 1 addition & 0 deletions foreman.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
rojo = { source = "rojo-rbx/rojo", version = "6.2.0" }
selene = { source = "Kampfkarren/selene", version = "0.18.1" }
stylua = { source = "JohnnyMorganz/StyLua", version = "0.13.1" }
luau-analyze = { source = "JohnnyMorganz/luau-analyze-rojo", version = "0.527.0" }
5 changes: 5 additions & 0 deletions src/.luaurc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"languageMode": "nonstrict",
"lint": { "*": true },
"lintErrors": true
}
5 changes: 2 additions & 3 deletions src/NoYield.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
--!nocheck

--!strict
--[[
Calls a function and throws an error if it attempts to yield.
Expand All @@ -9,7 +8,7 @@
given function will be returned.
]]

local function resultHandler(co, ok, ...)
local function resultHandler(co: thread, ok: boolean, ...)
if not ok then
local message = (...)
error(debug.traceback(co, message), 2)
Expand Down
16 changes: 14 additions & 2 deletions src/Signal.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
--[[
A limited, simple implementation of a Signal.
Expand Down Expand Up @@ -31,11 +32,22 @@ local function immutableRemoveValue(list, removeValue)
return new
end

type Listener = {
callback: (...any) -> (),
disconnected: boolean,
connectTraceback: string,
disconnectTraceback: string?,
}

type Store = {
_isDispatching: boolean,
}

local Signal = {}

Signal.__index = Signal

function Signal.new(store)
function Signal.new(store: Store?)
local self = {
_listeners = {},
_store = store,
Expand All @@ -59,7 +71,7 @@ function Signal:connect(callback)
)
end

local listener = {
local listener: Listener = {
callback = callback,
disconnected = false,
connectTraceback = debug.traceback(),
Expand Down
2 changes: 1 addition & 1 deletion src/Signal.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ return function()
it("should throw an error if the argument to `connect` is not a function", function()
local signal = Signal.new()
expect(function()
signal:connect("not a function")
signal:connect("not a function" :: any)
end).to.throw()
end)

Expand Down
21 changes: 14 additions & 7 deletions src/Store.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,13 @@ return function()
expect(caughtState.Value).to.equal(1)
expect(caughtAction.type).to.equal("@@INIT")
expect(caughtErrorResult.message).to.equal("Caught error in reducer with init")
expect(string.find(caughtErrorResult.thrownValue, innerErrorMessage)).to.be.ok()
local found = string.find(caughtErrorResult.thrownValue, innerErrorMessage)
expect(found).to.be.ok()
-- We want to verify that this is a stacktrace without caring too
-- much about the format, so we look for the stack frame associated
-- with this test file
expect(string.find(caughtErrorResult.thrownValue, script.Name)).to.be.ok()
found = string.find(caughtErrorResult.thrownValue, script.Name)
expect(found).to.be.ok()

store:destruct()
end)
Expand Down Expand Up @@ -218,11 +220,13 @@ return function()
expect(caughtState.Value).to.equal(2)
expect(caughtAction.type).to.equal("ThrowError")
expect(caughtErrorResult.message).to.equal("Caught error in reducer")
expect(string.find(caughtErrorResult.thrownValue, innerErrorMessage)).to.be.ok()
local found = string.find(caughtErrorResult.thrownValue, innerErrorMessage)
expect(found).to.be.ok()
-- We want to verify that this is a stacktrace without caring too
-- much about the format, so we look for the stack frame associated
-- with this test file
expect(string.find(caughtErrorResult.thrownValue, script.Name)).to.be.ok()
found = string.find(caughtErrorResult.thrownValue, script.Name)
expect(found).to.be.ok()

store:destruct()
end)
Expand Down Expand Up @@ -396,14 +400,16 @@ return function()
-- We want to verify that this is a stacktrace without caring too
-- much about the format, so we look for the stack frame associated
-- with this test file
expect(string.find(reportedErrorError, script.Name)).to.be.ok()
local found = string.find(reportedErrorError, script.Name)
expect(found).to.be.ok()
-- In vanilla lua, we get this message:
-- "attempt to yield across metamethod/C-call boundary"
-- In luau, we should end up wrapping our own NoYield message:
-- "Attempted to yield inside changed event!"
-- For convenience's sake, we just look for the common substring
local caughtErrorSubstring = "to yield"
expect(string.find(reportedErrorError, caughtErrorSubstring)).to.be.ok()
found = string.find(reportedErrorError, caughtErrorSubstring)
expect(found).to.be.ok()

store:destruct()
end)
Expand Down Expand Up @@ -479,7 +485,8 @@ return function()
-- We want to verify that this is a stacktrace without caring too
-- much about the format, so we look for the stack frame associated
-- with this test file
expect(string.find(caughtErrorResult.thrownValue, script.Name)).to.be.ok()
local found = string.find(caughtErrorResult.thrownValue, script.Name)
expect(found).to.be.ok()

expect(caughtActionLog[1]).to.equal(actions[1])
expect(caughtActionLog[2]).to.equal(actions[2])
Expand Down
1 change: 1 addition & 0 deletions src/combineReducers.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
--[[
Create a composite reducer from a map of keys and sub-reducers.
]]
Expand Down
1 change: 1 addition & 0 deletions src/createReducer.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
local actions = require(script.Parent.types.actions)
local reducers = require(script.Parent.types.reducers)

Expand Down
8 changes: 8 additions & 0 deletions src/init.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
local Store = require(script.Store)
local createReducer = require(script.createReducer)
local combineReducers = require(script.combineReducers)
Expand All @@ -8,13 +9,20 @@ local makeThunkMiddleware = require(script.makeThunkMiddleware)

local actions = require(script.types.actions)
local reducers = require(script.types.reducers)
local store = require(script.types.store)
local thunks = require(script.types.thunks)

export type Action<Type = any> = actions.Action<Type>
export type AnyAction = actions.AnyAction
export type ActionCreator<Type, Action, Args...> = actions.ActionCreator<Type, Action, Args...>

export type Reducer<State = any, Action = AnyAction> = reducers.Reducer<State, Action>

export type Store<State = any> = store.Store<State>

export type ThunkAction<ReturnType, State = any> = thunks.ThunkAction<ReturnType, State>
export type ThunkfulStore<State = any> = thunks.ThunkfulStore<State>

return {
Store = Store,
createReducer = createReducer,
Expand Down
5 changes: 4 additions & 1 deletion src/loggerMiddleware.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
--!strict
-- We want to be able to override outputFunction in tests, so the shape of this
-- module is kind of unconventional.
--
-- We fix it this weird shape in init.lua.
type OutputFunction = (...any) -> ()

local prettyPrint = require(script.Parent.prettyPrint)
local loggerMiddleware = {
outputFunction = print,
outputFunction = (print :: any) :: OutputFunction,
}

function loggerMiddleware.middleware(nextDispatch, store)
Expand Down
1 change: 1 addition & 0 deletions src/makeActionCreator.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
--[[
A helper function to define a Rodux action creator with an associated name.
]]
Expand Down
4 changes: 2 additions & 2 deletions src/makeActionCreator.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ return function()

it("should throw if the second argument is not a function", function()
expect(function()
makeActionCreator("foo", nil)
makeActionCreator("foo", nil :: any)
end).to.throw()

expect(function()
makeActionCreator("foo", {})
makeActionCreator("foo", {} :: any)
end).to.throw()
end)
end
1 change: 1 addition & 0 deletions src/makeThunkMiddleware.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
--[[
A middleware that allows for functions to be dispatched with an extra
argument for convenience. Functions will receive two arguments:
Expand Down
5 changes: 3 additions & 2 deletions src/prettyPrint.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
--!strict
local indent = " "

local function prettyPrint(value, indentLevel)
indentLevel = indentLevel or 0
local function prettyPrint(value, _indentLevel: number?)
local indentLevel = _indentLevel or 0
local output = {}

if typeof(value) == "table" then
Expand Down
1 change: 1 addition & 0 deletions src/thunkMiddleware.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
--[[
A middleware that allows for functions to be dispatched.
Functions will receive a single argument, the store itself.
Expand Down
1 change: 1 addition & 0 deletions src/types/actions.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
export type Action<Type = any> = {
type: Type,
}
Expand Down
1 change: 1 addition & 0 deletions src/types/reducers.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--!strict
local actions = require(script.Parent.actions)

type AnyAction = actions.AnyAction
Expand Down
17 changes: 17 additions & 0 deletions src/types/store.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
--!strict
local actions = require(script.Parent.actions)

type BaseAction = actions.Action<string>

type EmptyObject = {}

export type CombinedState<State> = EmptyObject & State

export type IDispatch<Store> = <Action>(self: Store, action: Action & BaseAction) -> ()
export type Dispatch<State = any> = IDispatch<Store<State>>

export type IStore<State, Dispatch> = {
dispatch: Dispatch,
getState: (self: IStore<State, Dispatch>) -> State,
destruct: (self: IStore<State, Dispatch>) -> (),
flush: (self: IStore<State, Dispatch>) -> (),
changed: RBXScriptSignal,
}
export type Store<State = any> = IStore<State, Dispatch<State>>

return nil
18 changes: 18 additions & 0 deletions src/types/thunks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--!strict
local store = require(script.Parent.store)

type IStore<State, Dispatch> = store.IStore<State, Dispatch>
type IDispatch<Store> = store.IDispatch<Store>

export type IThunkAction<ReturnType, Store> = (store: Store) -> ReturnType
export type ThunkAction<ReturnType, State = any> = IThunkAction<ReturnType, ThunkfulStore<State>>

export type IThunkDispatch<Store> = <ReturnType>(
self: Store,
thunkAction: IThunkAction<ReturnType, Store>
) -> ReturnType
export type ThunkDispatch<State = any> = IDispatch<ThunkfulStore<State>> & IThunkDispatch<ThunkfulStore<State>>

export type ThunkfulStore<State = any> = IStore<State, ThunkDispatch<State>>

return nil
24 changes: 24 additions & 0 deletions testez.d.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
declare function afterAll(callback: () -> ()): ()
declare function afterEach(callback: () -> ()): ()

declare function beforeAll(callback: () -> ()): ()
declare function beforeEach(callback: () -> ()): ()

declare function describe(phrase: string, callback: () -> ()): ()
declare function describeFOCUS(phrase: string, callback: () -> ()): ()
declare function fdescribe(phrase: string, callback: () -> ()): ()
declare function describeSKIP(phrase: string, callback: () -> ()): ()
declare function xdescribe(phrase: string, callback: () -> ()): ()

declare function expect(value: any): any

declare function FIXME(optionalMessage: string?): ()
declare function FOCUS(): ()
declare function SKIP(): ()

declare function it(phrase: string, callback: () -> ()): ()
declare function itFOCUS(phrase: string, callback: () -> ()): ()
declare function fit(phrase: string, callback: () -> ()): ()
declare function itSKIP(phrase: string, callback: () -> ()): ()
declare function xit(phrase: string, callback: () -> ()): ()
declare function itFIXME(phrase: string, callback: () -> ()): ()

0 comments on commit ce63e3d

Please sign in to comment.