Last active
August 26, 2018 23:55
-
-
Save bkonkle/e6af8dffe34cd32aee3aac7b14837834 to your computer and use it in GitHub Desktop.
Reasonable Redux
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open StateTypes; | |
let toOption = Js.Nullable.toOption; | |
let (getWithDefault, getExn) = Js.Option.(getWithDefault, getExn); | |
[@bs.module "redux"] external createStore : ('a, 'b, 'c) => 'd = ""; | |
[@bs.module "redux"] external applyMiddleware : ('a, 'b) => 'c = ""; | |
let identity = a => a; | |
let initialState: state = { | |
"account": AccountState.initialState, | |
"user": UserState.initialState, | |
}; | |
let rootReducer = (state: state, reduxAction) => | |
switch (reduxAction##action |> Js.Nullable.toOption) { | |
| Some(action) => | |
BsAbstract.Option.Infix.( | |
AccountState.reducer(state##account, action) | |
>>= ( | |
accountState => Some({"account": accountState, "user": state##user}) | |
) | |
<|> ( | |
UserState.reducer(state##user, action) | |
>>= ( | |
userState => Some({"user": userState, "account": state##account}) | |
) | |
) | |
|> Js.Option.getWithDefault(state) | |
) | |
| None => state | |
}; | |
let store = | |
createStore( | |
rootReducer, | |
initialState, | |
applyMiddleware( | |
StateMiddleware.handleActions, | |
StateMiddleware.handleActors(~actors=[|AccountState.accountCreation|]), | |
), | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type action = ..; | |
module Account = { | |
[@bs.deriving jsConverter] | |
type accountType = [ | |
| [@bs.as "account-type:parent-guardian"] `Parent | |
| [@bs.as "account-type:caregiver"] `Caregiver | |
]; | |
[@bs.deriving jsConverter] | |
type serviceType = [ | |
| [@bs.as "service-type:childcare"] `Childcare | |
| [@bs.as "service-type:tutoring"] `Tutoring | |
]; | |
type specialNeed = { | |
label: string, | |
id: int, | |
}; | |
type specialization = { | |
label: string, | |
id: int, | |
}; | |
[@bs.deriving jsConverter] | |
type state = { | |
email: option(string), | |
password: option(string), | |
displayName: option(string), | |
city: option(string), | |
accountType: option(accountType), | |
serviceType: option(serviceType), | |
specialNeeds: array(specialNeed), | |
specializations: array(specialization), | |
bio: option(string), | |
errors: array(string), | |
}; | |
type action += | |
| SetCredentials(string, string) | |
| SetProfile(string, string) | |
| SetAccountType(accountType) | |
| SetServiceType(serviceType) | |
| SetSpecialNeeds(array(specialNeed)) | |
| SetSpecializations(array(specialization)) | |
| SetBio(string) | |
| SetErrors(array(string)) | |
| SaveAccount; | |
}; | |
module User = { | |
[@bs.deriving jsConverter] | |
type state = {isAuthenticated: bool}; | |
type action += | |
| SetIsAuthenticated(bool); | |
}; | |
type reduxAction = { | |
. | |
"type": string, | |
"action": action, | |
}; | |
type state = { | |
. | |
"account": Account.state, | |
"user": User.state, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open StateTypes; | |
/** | |
* A convenience function to make un-curried middleware creation for Redux easier | |
*/ | |
let makeMiddleware = (~handler) => | |
(. store) => (. next) => (. action) => handler(~store, ~next, ~action); | |
let handleActors = (~actors) => | |
makeMiddleware(~handler=(~store, ~next, ~action) => { | |
let (dispatch, getState) = (store##dispatch, store##getState); | |
let result = next(. action); | |
actors |> Js.Array.forEach(actor => actor(~action, ~dispatch, ~getState)); | |
result; | |
}); | |
type reduxAction = { | |
. | |
"action": action, | |
"type": string, | |
}; | |
type actionTransformer = | |
(. action) => | |
(. ((. reduxAction) => reduxAction)) => (. action) => reduxAction; | |
let handleActions: actionTransformer = | |
makeMiddleware(~handler=(~store as _store, ~next, ~action: action) => { | |
open BsAbstract.Option.Infix; | |
let actionType = | |
AccountState.getActionType(action) | |
<|> UserState.getActionType(action) | |
|> Js.Option.getWithDefault("UNKNOWN"); | |
next(. {"type": actionType, "action": action}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let compose = (second, first, errors) => { | |
let result = first(errors); | |
Array.length(result) > 0 ? result : second(result); | |
}; | |
let isString = input => Js.typeof(input) === "string"; | |
let check = (condition, error) => | |
Array.append(condition() ? [||] : [|error|]); | |
let checkExists = input => check(() => ! Js.Nullable.isNullable(input)); | |
let checkIsSome = input => check(() => Js.Option.isSome(input)); | |
let checkIsString = input => check(() => isString(input)); | |
let checkRegex = (re, input: Js.nullable(string)) => | |
check(() => | |
Js.Nullable.isNullable(input) ? | |
false : | |
Js.Re.test(input |> Js.Nullable.toOption |> Js.Option.getExn, re) | |
); | |
let checkLength = (length, input, error) => | |
checkExists(input, error) | |
|> compose( | |
check( | |
() => | |
Js.Nullable.isNullable(input) ? | |
false : | |
String.length(input |> Js.Nullable.toOption |> Js.Option.getExn) | |
> length, | |
error, | |
), | |
); | |
let checkArray = (test, error, array) => | |
check(() => Js.Array.isArray(array), error) | |
|> compose( | |
check( | |
() => | |
Js.Array.isArray(array) ? | |
Js.Array.reduce( | |
(valid, value) => | |
/* Short-circuit failure */ | |
if (! valid) { | |
false; | |
} else { | |
test(value); | |
}, | |
true, | |
array, | |
) : | |
false, | |
error, | |
), | |
); | |
let validate = (success, error, checks) => { | |
let errors = [||] |> checks; | |
let invalid = Array.length(errors) > 0; | |
invalid ? error(errors) : success(); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open StateTypes.Account; | |
let toOption = Js.Nullable.toOption; | |
let (getWithDefault, getExn) = Js.Option.(getWithDefault, getExn); | |
let (assign, empty) = Js.Obj.(assign, empty); | |
let unsafeGet = a => a |> toOption |> getExn; | |
let initialState = { | |
email: None, | |
password: None, | |
displayName: None, | |
city: None, | |
accountType: None, | |
serviceType: None, | |
specialNeeds: [||], | |
specializations: [||], | |
bio: None, | |
errors: [||], | |
}; | |
let actions = | |
StateValidation.( | |
{ | |
"setErrors": errors => | |
validate( | |
() => SetErrors(errors), | |
errors => SetErrors(errors), | |
checkArray(isString, "Non-string errors received.", errors), | |
), | |
"setCredentials": | |
(. email, password) => | |
validate( | |
() => SetCredentials(email |> unsafeGet, password |> unsafeGet), | |
errors => SetErrors(errors), | |
checkExists(email, "A valid email is required.") | |
|> compose( | |
checkRegex( | |
[%bs.re {|/\S+@\S+\.\S+/|}], | |
email, | |
"A valid email is required.", | |
), | |
) | |
|> compose( | |
checkLength( | |
7, | |
password, | |
"A password at least 8 characters long is required.", | |
), | |
), | |
), | |
"setProfile": | |
(. displayName: Js.nullable(string), city: Js.nullable(string)) => | |
validate( | |
() => SetProfile(displayName |> unsafeGet, city |> unsafeGet), | |
errors => SetErrors(errors), | |
checkExists(displayName, "A display name is required."), | |
), | |
"setAccountType": | |
(. accountTypeJs) => { | |
let accountType = accountTypeFromJs(accountTypeJs); | |
validate( | |
() => SetAccountType(getExn(accountType)), | |
errors => SetErrors(errors), | |
checkIsSome( | |
accountType, | |
"An account type of Parent or Caregiver is required.", | |
), | |
); | |
}, | |
"setServiceType": | |
(. serviceTypeJs) => { | |
let serviceType = serviceTypeFromJs(serviceTypeJs); | |
validate( | |
() => SetServiceType(getExn(serviceType)), | |
errors => SetErrors(errors), | |
checkIsSome( | |
serviceType, | |
"A service type of Childcare or Tutoring is required.", | |
), | |
); | |
}, | |
"setSpecialNeeds": | |
(. specialNeeds: array(specialNeed)) => | |
validate( | |
() => SetSpecialNeeds(specialNeeds), | |
errors => SetErrors(errors), | |
checkArray(isString, "Non-string input received.", specialNeeds), | |
), | |
"setSpecializations": | |
(. specializations: array(specialization)) => | |
validate( | |
() => SetSpecializations(specializations), | |
errors => SetErrors(errors), | |
checkArray( | |
isString, | |
"Non-string input received.", | |
specializations, | |
), | |
), | |
"setBio": | |
(. bio: Js.nullable(string)) => | |
validate( | |
() => SetBio(bio |> unsafeGet), | |
errors => SetErrors(errors), | |
checkExists(bio, "A bio is required for this action."), | |
), | |
"saveAccount": () => SaveAccount, | |
} | |
); | |
let getActionType = (action: StateTypes.action) => | |
switch (action) { | |
| SetErrors(_) => Some("Account.setErrors") | |
| SetCredentials(_, _) => Some("Account.setCredentials") | |
| SetProfile(_, _) => Some("Account.setProfile") | |
| SetAccountType(_) => Some("Account.setAccountType") | |
| SetServiceType(_) => Some("Account.setServiceType") | |
| SetSpecialNeeds(_) => Some("Account.setSpecialNeeds") | |
| SetSpecializations(_) => Some("Account.setSpecializations") | |
| SetBio(_) => Some("Account.setBio") | |
| SaveAccount => Some("Account.saveAccount") | |
| _ => None | |
}; | |
let reducer = (state, action) => | |
switch (action) { | |
| SetCredentials(email, password) => | |
Some({ | |
...state, | |
errors: [||], | |
email: Some(email), | |
password: Some(password), | |
}) | |
| SetProfile(displayName, city) => | |
Some({ | |
...state, | |
errors: [||], | |
displayName: Some(displayName), | |
city: Some(city), | |
}) | |
| SetAccountType(accountType) => | |
Some({...state, errors: [||], accountType: Some(accountType)}) | |
| SetServiceType(serviceType) => | |
Some({...state, errors: [||], serviceType: Some(serviceType)}) | |
| SetSpecialNeeds(specialNeeds) => Some({...state, specialNeeds}) | |
| SetSpecializations(specializations) => | |
Some({...state, errors: [||], specializations}) | |
| SetBio(bio) => Some({...state, errors: [||], bio: Some(bio)}) | |
| SetErrors(errors) => Some({...state, errors}) | |
| _ => None | |
}; | |
module Selectors = { | |
let getState = (state: StateTypes.state) => stateToJs(state##account); | |
let getErrors = state => getState(state)##errors; | |
}; | |
let accountCreation = | |
(~action, ~dispatch as _dispatch, ~getState as _getState) => | |
switch (action##action |> toOption) { | |
| Some(SaveAccount) => Js.log("Time to save the account!!") | |
| _ => () | |
}; | |
let default = { | |
"actions": actions, | |
"selectors": { | |
"getState": Selectors.getState, | |
"getErrors": Selectors.getErrors, | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open StateTypes.User; | |
let getActionType = (action: StateTypes.action) => | |
switch (action) { | |
| SetIsAuthenticated(_) => Some("User.setIsAuthenticated") | |
| _ => None | |
}; | |
let initialState = {isAuthenticated: false}; | |
let actions = { | |
"setIsAuthenticated": isAuthenticated => | |
SetIsAuthenticated(isAuthenticated), | |
}; | |
let reducer = (_state, action) => | |
switch (action) { | |
| SetIsAuthenticated(isAuthenticated) => | |
Some({isAuthenticated: isAuthenticated}) | |
| _ => None | |
}; | |
module Selectors = { | |
let getState = (state: StateTypes.state) => stateToJs(state##user); | |
let getIsAuthenticated = state => getState(state)##isAuthenticated; | |
}; | |
let default = { | |
"actions": actions, | |
"selectors": { | |
"getState": Selectors.getState, | |
"getIsAuthenticated": Selectors.getIsAuthenticated, | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment