diff --git a/fsm/fsm/fsm.go b/fsm/fsm/fsm.go index 18918b0..6b178d9 100644 --- a/fsm/fsm/fsm.go +++ b/fsm/fsm/fsm.go @@ -2,6 +2,8 @@ package fsm import ( "errors" + "fmt" + "strings" "sync" ) @@ -63,7 +65,7 @@ type trEvent struct { isInternal bool } -type EventDesc struct { +type Event struct { Name string SrcState []string @@ -80,15 +82,16 @@ type Callback func(event string, args ...interface{}) (interface{}, error) type Callbacks map[string]Callback // TODO: Exports -func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]Callback) *FSM { - // Add validation, chains building +func MustNewFSM(machineName, initialState string, events []Event, callbacks map[string]Callback) *FSM { + machineName = strings.TrimSpace(machineName) + initialState = strings.TrimSpace(initialState) - if name == "" { - panic("name cannot be empty") + if machineName == "" { + panic("machine name cannot be empty") } - if initial == "" { - panic("initialState state cannot be empty") + if initialState == "" { + panic("initial state state cannot be empty") } // to remove @@ -97,9 +100,9 @@ func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]C } f := &FSM{ - name: name, - currentState: initial, - initialState: initial, + name: machineName, + currentState: initialState, + initialState: initialState, transitions: make(map[trKey]*trEvent), finStates: make(map[string]bool), callbacks: make(map[string]Callback), @@ -113,28 +116,33 @@ func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]C // Validate events for _, event := range events { + event.Name = strings.TrimSpace(event.Name) + event.DstState = strings.TrimSpace(event.DstState) if event.Name == "" { panic("cannot init empty event") } - // TODO: Check transition when all events added - if len(event.SrcState) == 0 { - panic("event must have min one source available state") - } - if event.DstState == "" { panic("event dest cannot be empty, use StateGlobalDone for finish or external state") } if _, ok := allEvents[event.Name]; ok { - panic("duplicate event") + panic(fmt.Sprintf("duplicate event \"%s\"", event.Name)) } allEvents[event.Name] = true allStates[event.DstState] = true + trimmedSourcesCounter := 0 + for _, sourceState := range event.SrcState { + sourceState := strings.TrimSpace(sourceState) + + if sourceState == "" { + continue + } + tKey := trKey{ sourceState, event.Name, @@ -151,7 +159,7 @@ func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]C f.transitions[tKey] = &trEvent{event.DstState, event.IsInternal} // For using provider, event must use with IsGlobal = true - if sourceState == initial { + if sourceState == initialState { if f.initialEvent != "" { panic("machine entry event already exist") } @@ -159,6 +167,11 @@ func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]C } allSources[sourceState] = true + trimmedSourcesCounter++ + } + + if trimmedSourcesCounter == 0 { + panic("event must have minimum one source available state") } } @@ -169,7 +182,7 @@ func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]C // Validate callbacks for event, callback := range callbacks { if event == "" { - panic("callback name cannot be empty") + panic("callback machineName cannot be empty") } if _, ok := allEvents[event]; !ok { @@ -275,18 +288,30 @@ func (f *FSM) EntryEvent() (event string) { } func (f *FSM) EventsList() (events []string) { + var eventsMap = map[string]bool{} if len(f.transitions) > 0 { for trKey, trEvent := range f.transitions { if !trEvent.isInternal { - events = append(events, trKey.event) + eventsMap[trKey.event] = true + if _, exists := eventsMap[trKey.event]; !exists { + + events = append(events, trKey.event) + } } } } + + if len(eventsMap) > 0 { + for event := range eventsMap { + events = append(events, event) + } + } + return } -func (f *FSM) StatesList() (states []string) { - allStates := map[string]bool{} +func (f *FSM) StatesSourcesList() (states []string) { + var allStates = map[string]bool{} if len(f.transitions) > 0 { for trKey, _ := range f.transitions { allStates[trKey.source] = true diff --git a/fsm/fsm/fsm_machines_data_test.go b/fsm/fsm/fsm_machines_data_test.go new file mode 100644 index 0000000..ea3821e --- /dev/null +++ b/fsm/fsm/fsm_machines_data_test.go @@ -0,0 +1,77 @@ +package fsm + +const ( + FSM1Name = "fsm1" + // Init process from global idle state + FSM1StateInit = StateGlobalIdle + // Set up data + FSM1StateStage1 = "state_fsm1_stage1" + // Process data + FSM1StateStage2 = "state_fsm1_stage2" + // Cancelled with internal event + FSM1StateCanceledByInternal = "state_fsm1_canceled1" + // Cancelled with external event + FSM1StateCanceled2 = "state_fsm1_canceled2" + // Out endpoint to switch + FSM1StateOutToFSM2 = "state_fsm1_out_to_fsm2" + FSM1StateOutToFSM3 = "state_fsm1_out_to_fsm3" + + // Events + EventFSM1Init = "event_fsm1_init" + EventFSM1Cancel = "event_fsm1_cancel" + EventFSM1Process = "event_fsm1_process" + + // Internal events + EventFSM1Internal = "event_internal_fsm1" + EventFSM1CancelByInternal = "event_internal_fsm1_cancel" + EventFSM1InternalOut2 = "event_internal_fsm1_out" +) + +var ( + testingEvents = []Event{ + // Init + {Name: EventFSM1Init, SrcState: []string{FSM1StateInit}, DstState: FSM1StateStage1}, + {Name: EventFSM1Internal, SrcState: []string{FSM1StateStage1}, DstState: FSM1StateStage2, IsInternal: true}, + + // Cancellation events + {Name: EventFSM1CancelByInternal, SrcState: []string{FSM1StateStage2}, DstState: FSM1StateCanceledByInternal, IsInternal: true}, + {Name: EventFSM1Cancel, SrcState: []string{FSM1StateStage2}, DstState: FSM1StateCanceled2}, + + // Out + {Name: EventFSM1Process, SrcState: []string{FSM1StateStage2}, DstState: FSM1StateOutToFSM2}, + {Name: EventFSM1InternalOut2, SrcState: []string{FSM1StateStage2}, DstState: FSM1StateOutToFSM3, IsInternal: true}, + } + + testingCallbacks = Callbacks{ + EventFSM1Init: actionSetUpData, + EventFSM1InternalOut2: actionEmitOut2, + EventFSM1Process: actionProcessData, + } +) + +type testMachineFSM struct { + *FSM +} + +/*func new() fsm_pool.IStateMachine { + machine := &testMachineFSM{} + machine.FSM = MustNewFSM( + FSM1Name, + FSM1StateInit, + testingEvents, + testingCallbacks, + ) + return machine +}*/ + +func actionSetUpData(event string, args ...interface{}) (response interface{}, err error) { + return +} + +func actionProcessData(event string, args ...interface{}) (response interface{}, err error) { + return +} + +func actionEmitOut2(event string, args ...interface{}) (response interface{}, err error) { + return +} diff --git a/fsm/fsm/fsm_machines_test.go b/fsm/fsm/fsm_machines_test.go new file mode 100644 index 0000000..a3662c9 --- /dev/null +++ b/fsm/fsm/fsm_machines_test.go @@ -0,0 +1,225 @@ +package fsm + +import ( + "log" + "testing" +) + +var testingFSM *FSM + +func init() { + testingFSM = MustNewFSM( + FSM1Name, + FSM1StateInit, + testingEvents, + testingCallbacks, + ) +} + +func compareRecoverStr(t *testing.T, r interface{}, assertion string) { + if r == nil { + return + } + msg, ok := r.(string) + if !ok { + t.Error("not asserted recover:", r) + } + if msg != assertion { + t.Error("not asserted recover:", msg) + } +} + +func compareArrays(src, dst []string) bool { + if len(src) != len(dst) { + return false + } + // create a map of string -> int + diff := make(map[string]int, len(src)) + for _, _x := range src { + // 0 value for int is 0, so just increment a counter for the string + diff[_x]++ + } + for _, _y := range dst { + // If the string _y is not in diff bail out early + if _, ok := diff[_y]; !ok { + return false + } + diff[_y] -= 1 + if diff[_y] == 0 { + delete(diff, _y) + } + } + if len(diff) == 0 { + return true + } + return false +} + +func TestMustNewFSM_Empty_Name_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "machine name cannot be empty") + }() + testingFSM = MustNewFSM( + "", + "init_state", + []Event{}, + nil, + ) + + t.Errorf("did not panic on empty machine name") +} + +func TestMustNewFSM_Empty_Initial_State_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "initial state state cannot be empty") + }() + + testingFSM = MustNewFSM( + "fsm", + "", + []Event{}, + nil, + ) + + t.Errorf("did not panic on empty initial") +} + +func TestMustNewFSM_Empty_Events_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "cannot init fsm with empty events") + }() + + testingFSM = MustNewFSM( + "fsm", + "init_state", + []Event{}, + nil, + ) + + t.Errorf("did not panic on empty events list") +} + +func TestMustNewFSM_Event_Empty_Name_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "cannot init empty event") + }() + + testingFSM = MustNewFSM( + "fsm", + "init_state", + []Event{ + {Name: "", SrcState: []string{"init_state"}, DstState: StateGlobalDone}, + }, + nil, + ) + + t.Errorf("did not panic on empty event name") +} + +func TestMustNewFSM_Event_Empty_Source_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "event must have minimum one source available state") + }() + + testingFSM = MustNewFSM( + "fsm", + "init_state", + []Event{ + {Name: "event", SrcState: []string{}, DstState: StateGlobalDone}, + }, + nil, + ) + + t.Errorf("did not panic on empty event sources") +} + +func TestMustNewFSM_States_Min_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "machine must contain at least two states") + }() + + testingFSM = MustNewFSM( + "fsm", + "init_state", + []Event{ + {Name: "event", SrcState: []string{"init_state"}, DstState: StateGlobalDone}, + }, + nil, + ) + + t.Errorf("did not panic on less than two states") +} + +func TestMustNewFSM_State_Entry_Conflict_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "machine entry event already exist") + }() + + testingFSM = MustNewFSM( + "fsm", + "init_state", + []Event{ + {Name: "event1", SrcState: []string{"init_state"}, DstState: "state"}, + {Name: "event2", SrcState: []string{"init_state"}, DstState: "state"}, + }, + nil, + ) + + t.Errorf("did not panic on initialize with conflict in entry state") +} + +func TestMustNewFSM_State_Final_Not_Found_Panic(t *testing.T) { + defer func() { + compareRecoverStr(t, recover(), "cannot initialize machine without final states") + }() + + testingFSM = MustNewFSM( + "fsm", + "init_state", + []Event{ + {Name: "event1", SrcState: []string{"init_state"}, DstState: "state2"}, + {Name: "event2", SrcState: []string{"state2"}, DstState: "init_state"}, + }, + nil, + ) + + t.Errorf("did not panic on initialize without final state") +} + +func TestFSM_Name(t *testing.T) { + if testingFSM.Name() != FSM1Name { + t.Errorf("expected machine name \"%s\"", FSM1Name) + } +} + +func TestFSM_EntryEvent(t *testing.T) { + if testingFSM.InitialState() != FSM1StateInit { + t.Errorf("expected initial state \"%s\"", FSM1StateInit) + } +} + +func TestFSM_EventsList(t *testing.T) { + eventsList := []string{ + EventFSM1Init, + EventFSM1Cancel, + EventFSM1Process, + } + + if !compareArrays(testingFSM.EventsList(), eventsList) { + t.Error("expected public events", eventsList) + } + +} + +func TestFSM_StatesList(t *testing.T) { + log.Println(testingFSM.StatesSourcesList()) + statesList := []string{ + FSM1StateInit, + FSM1StateStage2, + FSM1StateStage2, + } + + if !compareArrays(testingFSM.StatesSourcesList(), statesList) { + t.Error("expected stages", statesList) + } +} diff --git a/fsm/fsm_pool/fsm_pool.go b/fsm/fsm_pool/fsm_pool.go index f25047e..3b24ec5 100644 --- a/fsm/fsm_pool/fsm_pool.go +++ b/fsm/fsm_pool/fsm_pool.go @@ -21,7 +21,7 @@ type IStateMachine interface { EventsList() []string - StatesList() []string + StatesSourcesList() []string IsFinState(state string) bool } @@ -93,7 +93,7 @@ func Init(machines ...IStateMachine) *FSMPoolProvider { // Fill up states with initial and exit states checking for _, machine := range machines { machineName := machine.Name() - machineStates := machine.StatesList() + machineStates := machine.StatesSourcesList() for _, state := range machineStates { if machine.IsFinState(state) { // If state is initial for another machine, diff --git a/fsm/state_machines/signature_construct_fsm/init.go b/fsm/state_machines/signature_construct_fsm/init.go index 66702bb..5f80f06 100644 --- a/fsm/state_machines/signature_construct_fsm/init.go +++ b/fsm/state_machines/signature_construct_fsm/init.go @@ -25,7 +25,7 @@ func New() fsm_pool.IStateMachine { machine.FSM = fsm.MustNewFSM( fsmName, stateConstructorEntryPoint, - []fsm.EventDesc{ + []fsm.Event{ // {Name: "", SrcState: []string{""}, DstState: ""}, // Init diff --git a/fsm/state_machines/signature_proposal_fsm/init.go b/fsm/state_machines/signature_proposal_fsm/init.go index 3770b58..4ab0d18 100644 --- a/fsm/state_machines/signature_proposal_fsm/init.go +++ b/fsm/state_machines/signature_proposal_fsm/init.go @@ -36,7 +36,7 @@ func New() fsm_pool.IStateMachine { machine.FSM = fsm.MustNewFSM( fsmName, fsm.StateGlobalIdle, - []fsm.EventDesc{ + []fsm.Event{ // {Name: "", SrcState: []string{""}, DstState: ""}, // Init diff --git a/fsm/types/requests/signature_proposal.go b/fsm/types/requests/signature_proposal.go new file mode 100644 index 0000000..06f6b1c --- /dev/null +++ b/fsm/types/requests/signature_proposal.go @@ -0,0 +1,48 @@ +package requests + +import ( + "errors" + "github.com/p2p-org/dc4bc/fsm/config" +) + +// Requests + +type ProposalParticipantsListRequest []ProposalParticipantsEntryRequest + +type ProposalParticipantsEntryRequest struct { + // Public title for address, such as name, nickname, organization + Title string + PublicKey []byte +} + +func (r *ProposalParticipantsListRequest) Validate() error { + if len(*r) < config.ParticipantsMinCount { + return errors.New("too few participants") + } + + for _, participant := range *r { + if len(participant.Title) < 3 { + return errors.New("title too short") + } + + if len(participant.Title) > 150 { + return errors.New("title too long") + } + + if len(participant.PublicKey) < 10 { + return errors.New("pub key too short") + } + } + + return nil +} + +type ProposalParticipantConfirmationRequest struct { + // Public title for address, such as name, nickname, organization + ParticipantId string + EncryptedInvitation string +} + +func (r *ProposalParticipantConfirmationRequest) Validate() error { + return nil +} diff --git a/fsm/types/responses/signature_proposal.go b/fsm/types/responses/signature_proposal.go new file mode 100644 index 0000000..ff9acdc --- /dev/null +++ b/fsm/types/responses/signature_proposal.go @@ -0,0 +1,14 @@ +package responses + +// Responses + +type ProposalParticipantInvitationsResponse []*ProposalParticipantInvitationEntryResponse + +type ProposalParticipantInvitationEntryResponse struct { + // Public title for address, such as name, nickname, organization + Title string + // Key for link invitations to participants + PubKeyFingerprint string + // Encrypted with public key secret + EncryptedInvitation string +} diff --git a/go.mod b/go.mod index fdf8c13..695749c 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,16 @@ require ( github.com/looplab/fsm v0.1.0 github.com/makiuchi-d/gozxing v0.0.0-20190830103442-eaff64b1ceb7 github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5 - github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f // indirect + github.com/p2p-org/dc4bc v0.0.0-00010101000000-000000000000 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/stretchr/testify v1.6.1 // indirect + github.com/stretchr/testify v1.6.1 go.dedis.ch/kyber/v3 v3.0.9 gocv.io/x/gocv v0.23.0 golang.org/x/image v0.0.0-20200618115811-c13761719519 - golang.org/x/text v0.3.3 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect ) replace golang.org/x/crypto => github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5 replace go.dedis.ch/kyber/v3 => github.com/corestario/kyber/v3 v3.0.0-20200218082721-8ed10c357c05 + +replace github.com/p2p-org/dc4bc => /home/tellme/PROJECTS/go/src/github.com/p2p-org/dc4bc