Files
hanko/backend/flowpilot/builder.go
bjoern-m 601ffaae92 Introduce Flowpilot - integration (#1532)
This pull request introduces the new Flowpilot system along with several new features and various improvements. The key enhancements include configurable authorization, registration, and profile flows, as well as the ability to enable and disable user identifiers (e.g., email addresses and usernames) and login methods.

---------

Co-authored-by: Frederic Jahn <frederic.jahn@hanko.io>
Co-authored-by: Lennart Fleischmann <lennart.fleischmann@hanko.io>
Co-authored-by: lfleischmann <67686424+lfleischmann@users.noreply.github.com>
Co-authored-by: merlindru <hello@merlindru.com>
2024-08-06 16:07:29 +02:00

285 lines
8.5 KiB
Go

package flowpilot
import (
"errors"
"fmt"
"time"
)
type FlowBuilder interface {
TTL(ttl time.Duration) FlowBuilder
State(stateName StateName, actions ...Action) FlowBuilder
InitialState(stateNames ...StateName) FlowBuilder
ErrorState(stateName StateName) FlowBuilder
BeforeState(stateName StateName, hooks ...HookAction) FlowBuilder
AfterState(stateName StateName, hooks ...HookAction) FlowBuilder
AfterFlow(flowName FlowName, hooks ...HookAction) FlowBuilder
Debug(enabled bool) FlowBuilder
SubFlows(subFlows ...subFlow) FlowBuilder
Build() (Flow, error)
MustBuild() Flow
BeforeEachAction(hooks ...HookAction) FlowBuilder
AfterEachAction(hooks ...HookAction) FlowBuilder
}
// defaultFlowBuilderBase is the base flow builder struct.
type defaultFlowBuilderBase struct {
name FlowName
flow stateActions
subFlows SubFlows
stateDetails stateDetails
beforeStateHooks stateHooks
afterStateHooks stateHooks
beforeEachActionHooks hookActions
afterEachActionHooks hookActions
afterFlowHooks flowHooks
}
// defaultFlowBuilder is a builder struct for creating a new Flow.
type defaultFlowBuilder struct {
path string
ttl time.Duration
initialStateNames []StateName
errorStateName StateName
debug bool
defaultFlowBuilderBase
}
// newFlowBuilderBase creates a new defaultFlowBuilderBase instance.
func newFlowBuilderBase(name FlowName) defaultFlowBuilderBase {
return defaultFlowBuilderBase{
name: name,
flow: make(stateActions),
subFlows: make(SubFlows, 0),
stateDetails: make(stateDetails),
beforeStateHooks: make(stateHooks),
afterStateHooks: make(stateHooks),
afterFlowHooks: make(flowHooks),
}
}
// NewFlow creates a new defaultFlowBuilder that builds a new flow available under the specified path.
func NewFlow(name FlowName) FlowBuilder {
path := fmt.Sprintf("/%s", name)
fbBase := newFlowBuilderBase(name)
return &defaultFlowBuilder{path: path, defaultFlowBuilderBase: fbBase}
}
// TTL sets the time-to-live (TTL) for the flow.
func (fb *defaultFlowBuilder) TTL(ttl time.Duration) FlowBuilder {
fb.ttl = ttl
return fb
}
func (fb *defaultFlowBuilderBase) addState(stateName StateName, actions ...Action) {
fb.flow[stateName] = append(fb.flow[stateName], actions...)
}
func (fb *defaultFlowBuilderBase) addBeforeStateHooks(stateName StateName, hooks ...HookAction) {
fb.beforeStateHooks[stateName] = append(fb.beforeStateHooks[stateName], hooks...)
}
func (fb *defaultFlowBuilderBase) addAfterStateHooks(stateName StateName, hooks ...HookAction) {
fb.afterStateHooks[stateName] = append(fb.afterStateHooks[stateName], hooks...)
}
func (fb *defaultFlowBuilderBase) addAfterFlowHooks(flowName FlowName, hooks ...HookAction) {
fb.afterFlowHooks[flowName] = append(fb.afterFlowHooks[flowName], hooks...)
}
func (fb *defaultFlowBuilder) addBeforeEachActionHooks(hooks ...HookAction) {
fb.beforeEachActionHooks = append(fb.beforeEachActionHooks, hooks...)
}
func (fb *defaultFlowBuilder) addAfterEachActionHooks(hooks ...HookAction) {
fb.afterEachActionHooks = append(fb.afterEachActionHooks, hooks...)
}
func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...subFlow) {
fb.subFlows = append(fb.subFlows, subFlows...)
}
func (fb *defaultFlowBuilderBase) addStateIfNotExists(stateName StateName) {
if _, exists := fb.flow[stateName]; !exists {
fb.addState(stateName)
}
}
// scanFlowStates iterates through each state in the provided flow and associates relevant information, also it checks
// for uniqueness of state names.
func (fb *defaultFlowBuilder) scanFlowStates(flow flowBase) error {
// Iterate through states in the flow.
for stateName, actions := range flow.getFlow() {
// Check if state name is already in use.
if _, ok := fb.stateDetails[stateName]; ok {
continue
}
actionDetails := make(defaultActionDetails, len(actions))
for i, action := range actions {
actionDetails[i] = &defaultActionDetail{
action: action,
flowName: flow.getName(),
}
}
// Create state details.
state := &defaultStateDetail{
name: stateName,
actionDetails: actionDetails,
flow: flow.getFlow(),
subFlows: flow.getSubFlows(),
flowName: flow.getName(),
}
// Store state details.
fb.stateDetails[stateName] = state
}
for stateName, actions := range flow.getBeforeStateHooks() {
fb.beforeStateHooks[stateName] = append(fb.beforeStateHooks[stateName], actions...)
}
for stateName, actions := range flow.getAfterStateHooks() {
fb.afterStateHooks[stateName] = append(fb.afterStateHooks[stateName], actions...)
}
actions := flow.getAfterFlowHooks()
fb.afterFlowHooks[flow.getName()] = append(fb.afterFlowHooks[flow.getName()], actions...)
// Recursively scan sub-flows.
for _, sf := range flow.getSubFlows() {
if err := fb.scanFlowStates(sf); err != nil {
return err
}
}
return nil
}
// validate performs validation checks on the flow configuration.
func (fb *defaultFlowBuilder) validate() error {
// Validate fixed states and their presence in the flow.
if len(fb.initialStateNames) == 0 {
return errors.New("fixed state 'initialState' is not set")
}
if len(fb.errorStateName) == 0 {
return errors.New("fixed state 'errorState' is not set")
}
if !fb.flow.stateExists(fb.initialStateNames[0]) && !fb.subFlows.stateExists(fb.initialStateNames[0]) {
return fmt.Errorf("initial state '%s' does not belong to the flow or a sub-flow", fb.initialStateNames[0])
}
if !fb.flow.stateExists(fb.errorStateName) {
return fmt.Errorf("error state '%s' does not belong to the flow", fb.errorStateName)
}
return nil
}
// State adds a new state to the flow.
func (fb *defaultFlowBuilder) State(stateName StateName, actions ...Action) FlowBuilder {
fb.addState(stateName, actions...)
return fb
}
func (fb *defaultFlowBuilder) BeforeState(stateName StateName, hooks ...HookAction) FlowBuilder {
fb.addBeforeStateHooks(stateName, hooks...)
return fb
}
func (fb *defaultFlowBuilder) AfterState(stateName StateName, hooks ...HookAction) FlowBuilder {
fb.addAfterStateHooks(stateName, hooks...)
return fb
}
func (fb *defaultFlowBuilder) AfterFlow(flowName FlowName, hooks ...HookAction) FlowBuilder {
fb.addAfterFlowHooks(flowName, hooks...)
return fb
}
func (fb *defaultFlowBuilder) BeforeEachAction(hooks ...HookAction) FlowBuilder {
fb.addBeforeEachActionHooks(hooks...)
return fb
}
func (fb *defaultFlowBuilder) AfterEachAction(hooks ...HookAction) FlowBuilder {
fb.addAfterEachActionHooks(hooks...)
return fb
}
func (fb *defaultFlowBuilder) InitialState(nextStateNames ...StateName) FlowBuilder {
fb.initialStateNames = nextStateNames
return fb
}
func (fb *defaultFlowBuilder) ErrorState(stateName StateName) FlowBuilder {
fb.addStateIfNotExists(stateName)
fb.errorStateName = stateName
return fb
}
func (fb *defaultFlowBuilder) SubFlows(subFlows ...subFlow) FlowBuilder {
fb.addSubFlows(subFlows...)
return fb
}
// Debug enables the debug mode, which causes the flow response to contain the actual error.
func (fb *defaultFlowBuilder) Debug(enabled bool) FlowBuilder {
fb.debug = enabled
return fb
}
// Build constructs and returns the Flow object.
func (fb *defaultFlowBuilder) Build() (Flow, error) {
if err := fb.validate(); err != nil {
return nil, fmt.Errorf("flow validation failed: %w", err)
}
dfb := &defaultFlowBase{
name: fb.name,
flow: fb.flow,
subFlows: fb.subFlows,
beforeStateHooks: fb.beforeStateHooks,
afterStateHooks: fb.afterStateHooks,
beforeEachActionHooks: fb.beforeEachActionHooks,
afterEachActionHooks: fb.afterEachActionHooks,
afterFlowHooks: fb.afterFlowHooks,
}
flow := &defaultFlow{
initialStateNames: fb.initialStateNames,
errorStateName: fb.errorStateName,
stateDetails: fb.stateDetails,
ttl: fb.ttl,
debug: fb.debug,
defaultFlowBase: dfb,
contextValues: make(contextValues),
}
// Check if states were already scanned, if so, don't scan again
if len(fb.stateDetails) == 0 {
if err := fb.scanFlowStates(flow); err != nil {
return nil, fmt.Errorf("failed to scan flow states: %w", err)
}
}
flow.defaultFlowBase.beforeStateHooks.makeUnique()
flow.defaultFlowBase.afterStateHooks.makeUnique()
flow.defaultFlowBase.afterFlowHooks.makeUnique()
return flow, nil
}
// MustBuild constructs and returns the Flow object, panics on error.
func (fb *defaultFlowBuilder) MustBuild() Flow {
f, err := fb.Build()
if err != nil {
panic(err)
}
return f
}