Skip to content

Instantly share code, notes, and snippets.

@bkonkle
Last active August 26, 2018 23:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bkonkle/e6af8dffe34cd32aee3aac7b14837834 to your computer and use it in GitHub Desktop.
Save bkonkle/e6af8dffe34cd32aee3aac7b14837834 to your computer and use it in GitHub Desktop.
Reasonable Redux
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|]),
),
);
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,
};
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});
});
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();
};
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,
},
};
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