diff --git a/README.md b/README.md index ac37dfd..217ccf3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -
f-mesh

f-mesh

@@ -7,41 +6,33 @@

What is it?

-

F-Mesh is a simplistic FBP-inspired framework in Go. -It allows you to express your program as a mesh of interconnected components. -You can think of it as a simple functions orchestrator. +

F-Mesh is a functions orchestrator inspired by FBP. +It allows you to express your program as a mesh of interconnected components (or more formally as a computational graph).

Main concepts:

What it is not?

-

F-mesh is not a classical FBP implementation, and it is not fully async. It does not support long-running components or wall-time events (like timers and tickers)

-

The framework is not suitable for implementing complex concurrent systems

+

F-mesh is not a classical FBP implementation, it does not support long-running components or wall-time events (like timers and tickers)

+

Example:

```go - // Create f-mesh - fm := fmesh.New("hello world"). +fm := fmesh.New("hello world"). WithComponents( component.New("concat"). WithInputs("i1", "i2"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - word1 := inputs.ByName("i1").Signals().FirstPayload().(string) - word2 := inputs.ByName("i2").Signals().FirstPayload().(string) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + word1 := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + word2 := inputs.ByName("i2").FirstSignalPayloadOrDefault("").(string) outputs.ByName("res").PutSignals(signal.New(word1 + word2)) return nil @@ -49,24 +40,24 @@ You can think of it as a simple functions orchestrator. component.New("case"). WithInputs("i1"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - inputString := inputs.ByName("i1").Signals().FirstPayload().(string) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputString := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) outputs.ByName("res").PutSignals(signal.New(strings.ToTitle(inputString))) return nil })). - .WithConfig(fmesh.Config{ - ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, - CyclesLimit: 10, - }) + WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) fm.Components().ByName("concat").Outputs().ByName("res").PipeTo( fm.Components().ByName("case").Inputs().ByName("i1"), ) // Init inputs - fm.Components().ByName("concat").Inputs().ByName("i1").PutSignals(signal.New("hello ")) - fm.Components().ByName("concat").Inputs().ByName("i2").PutSignals(signal.New("world !")) + fm.Components().ByName("concat").InputByName("i1").PutSignals(signal.New("hello ")) + fm.Components().ByName("concat").InputByName("i2").PutSignals(signal.New("world !")) // Run the mesh _, err := fm.Run() @@ -78,8 +69,8 @@ You can think of it as a simple functions orchestrator. } //Extract results - results := fm.Components().ByName("case").Outputs().ByName("res").Signals().FirstPayload() - fmt.Printf("Result is :%v", results) + results := fm.Components().ByName("case").OutputByName("res").FirstSignalPayloadOrNil() + fmt.Printf("Result is : %v", results) ``` - -

Version 0.1.0 coming soon

+See more in ```examples``` directory. +

Version 0.1.0 coming soon

\ No newline at end of file diff --git a/common/chainable.go b/common/chainable.go new file mode 100644 index 0000000..756b468 --- /dev/null +++ b/common/chainable.go @@ -0,0 +1,25 @@ +package common + +type Chainable struct { + err error +} + +// NewChainable initialises new chainable +func NewChainable() *Chainable { + return &Chainable{} +} + +// SetErr sets chainable error +func (c *Chainable) SetErr(err error) { + c.err = err +} + +// HasErr returns true when chainable has error +func (c *Chainable) HasErr() bool { + return c.err != nil +} + +// Err returns chainable error +func (c *Chainable) Err() error { + return c.err +} diff --git a/common/described_entity.go b/common/described_entity.go new file mode 100644 index 0000000..df8fc7e --- /dev/null +++ b/common/described_entity.go @@ -0,0 +1,15 @@ +package common + +type DescribedEntity struct { + description string +} + +// NewDescribedEntity constructor +func NewDescribedEntity(description string) DescribedEntity { + return DescribedEntity{description: description} +} + +// Description getter +func (e DescribedEntity) Description() string { + return e.description +} diff --git a/common/described_entity_test.go b/common/described_entity_test.go new file mode 100644 index 0000000..91e5627 --- /dev/null +++ b/common/described_entity_test.go @@ -0,0 +1,65 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewDescribedEntity(t *testing.T) { + type args struct { + description string + } + tests := []struct { + name string + args args + want DescribedEntity + }{ + { + name: "empty description", + args: args{ + description: "", + }, + want: DescribedEntity{ + description: "", + }, + }, + { + name: "with description", + args: args{ + description: "component1 is used to generate logs", + }, + want: DescribedEntity{ + description: "component1 is used to generate logs", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, NewDescribedEntity(tt.args.description)) + }) + } +} + +func TestDescribedEntity_Description(t *testing.T) { + tests := []struct { + name string + describedEntity DescribedEntity + want string + }{ + { + name: "empty description", + describedEntity: NewDescribedEntity(""), + want: "", + }, + { + name: "with description", + describedEntity: NewDescribedEntity("component2 is used to handle errors"), + want: "component2 is used to handle errors", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.describedEntity.Description()) + }) + } +} diff --git a/common/labeled_entity.go b/common/labeled_entity.go new file mode 100644 index 0000000..c10ada6 --- /dev/null +++ b/common/labeled_entity.go @@ -0,0 +1,97 @@ +package common + +import ( + "errors" + "fmt" +) + +type LabelsCollection map[string]string + +type LabeledEntity struct { + labels LabelsCollection +} + +var ( + ErrLabelNotFound = errors.New("label not found") +) + +// NewLabeledEntity constructor +func NewLabeledEntity(labels LabelsCollection) LabeledEntity { + return LabeledEntity{labels: labels} +} + +// Labels getter +func (e *LabeledEntity) Labels() LabelsCollection { + return e.labels +} + +// Label returns the value of single label or nil if it is not found +func (e *LabeledEntity) Label(label string) (string, error) { + value, ok := e.labels[label] + + if !ok { + return "", fmt.Errorf("label %s not found, %w", label, ErrLabelNotFound) + } + + return value, nil +} + +// LabelOrDefault returns label value or default value in case of any error +func (e *LabeledEntity) LabelOrDefault(label string, defaultValue string) string { + value, err := e.Label(label) + if err != nil { + return defaultValue + } + return value +} + +// SetLabels overwrites labels collection +func (e *LabeledEntity) SetLabels(labels LabelsCollection) { + e.labels = labels +} + +// AddLabel adds or updates(if label already exists) single label +func (e *LabeledEntity) AddLabel(label string, value string) { + if e.labels == nil { + e.labels = make(LabelsCollection) + } + e.labels[label] = value +} + +// AddLabels adds or updates(if label already exists) multiple labels +func (e *LabeledEntity) AddLabels(labels LabelsCollection) { + for label, value := range labels { + e.AddLabel(label, value) + } +} + +// DeleteLabel deletes given label +func (e *LabeledEntity) DeleteLabel(label string) { + delete(e.labels, label) +} + +// HasLabel returns true when entity has given label or false otherwise +func (e *LabeledEntity) HasLabel(label string) bool { + _, ok := e.labels[label] + return ok +} + +// HasAllLabels checks if entity has all labels +func (e *LabeledEntity) HasAllLabels(label ...string) bool { + for _, l := range label { + if !e.HasLabel(l) { + return false + } + } + return true +} + +// HasAnyLabel checks if entity has at least one of given labels +func (e *LabeledEntity) HasAnyLabel(label ...string) bool { + for _, l := range label { + if e.HasLabel(l) { + return true + } + } + return false +} diff --git a/common/labeled_entity_test.go b/common/labeled_entity_test.go new file mode 100644 index 0000000..c514e2d --- /dev/null +++ b/common/labeled_entity_test.go @@ -0,0 +1,481 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewLabeledEntity(t *testing.T) { + type args struct { + labels LabelsCollection + } + tests := []struct { + name string + args args + want LabeledEntity + }{ + { + name: "empty labels", + args: args{ + labels: nil, + }, + want: LabeledEntity{ + labels: nil, + }, + }, + { + name: "with labels", + args: args{ + labels: LabelsCollection{ + "label1": "value1", + }, + }, + want: LabeledEntity{ + labels: LabelsCollection{ + "label1": "value1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, NewLabeledEntity(tt.args.labels)) + }) + } +} + +func TestLabeledEntity_Labels(t *testing.T) { + tests := []struct { + name string + labeledEntity LabeledEntity + want LabelsCollection + }{ + { + name: "no labels", + labeledEntity: NewLabeledEntity(nil), + want: nil, + }, + { + name: "with labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + want: LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.labeledEntity.Labels()) + }) + } +} + +func TestLabeledEntity_SetLabels(t *testing.T) { + type args struct { + labels LabelsCollection + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "setting to empty labels collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + labels: LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }, labeledEntity.Labels()) + }, + }, + { + name: "setting to non-empty labels collection", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + "l99": "val1", + }), + args: args{ + labels: LabelsCollection{ + "l4": "v4", + "l5": "v5", + "l6": "v6", + "l99": "val2", + }, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l4": "v4", + "l5": "v5", + "l6": "v6", + "l99": "val2", + }, labeledEntity.Labels()) + }, + }, + { + name: "setting nil", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + "l99": "val1", + }), + args: args{ + labels: nil, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Nil(t, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.SetLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_AddLabel(t *testing.T) { + type args struct { + label string + value string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "adding to empty labels collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + label: "l1", + value: "v1", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + }, labeledEntity.Labels()) + }, + }, + { + name: "adding to non-empty labels collection", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + }), + args: args{ + label: "l2", + value: "v2", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, labeledEntity.Labels()) + }, + }, + { + name: "overwriting a label", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l2", + value: "v3", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v3", + }, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.AddLabel(tt.args.label, tt.args.value) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_AddLabels(t *testing.T) { + type args struct { + labels LabelsCollection + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "adding to non-empty labels collection", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + labels: LabelsCollection{ + "l3": "v100", + "l4": "v4", + "l5": "v5", + }, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v100", + "l4": "v4", + "l5": "v5", + }, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.AddLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_DeleteLabel(t *testing.T) { + type args struct { + label string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "label found and deleted", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l1", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l2": "v2", + }, labeledEntity.Labels()) + }, + }, + { + name: "label not found, no-op", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l3", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.DeleteLabel(tt.args.label) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_HasAllLabels(t *testing.T) { + type args struct { + label []string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + want bool + }{ + { + name: "empty collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + label: []string{"l1"}, + }, + want: false, + }, + { + name: "has all labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l1", "l2"}, + }, + want: true, + }, + { + name: "does not have all labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l1", "l2", "l4"}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.labeledEntity.HasAllLabels(tt.args.label...)) + }) + } +} + +func TestLabeledEntity_HasAnyLabel(t *testing.T) { + type args struct { + label []string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + want bool + }{ + { + name: "empty collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + label: []string{"l1"}, + }, + want: false, + }, + { + name: "has some labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l1", "l10"}, + }, + want: true, + }, + { + name: "does not have any of labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l10", "l20", "l4"}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.labeledEntity.HasAnyLabel(tt.args.label...)) + }) + } +} + +func TestLabeledEntity_Label(t *testing.T) { + type args struct { + label string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + want string + wantErr bool + }{ + { + name: "no labels", + labeledEntity: LabeledEntity{ + labels: nil, + }, + args: args{ + label: "l1", + }, + want: "", + wantErr: true, + }, + { + name: "label found", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l2", + }, + want: "v2", + wantErr: false, + }, + { + name: "label not found", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l3", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.labeledEntity.Label(tt.args.label) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/common/named_entity.go b/common/named_entity.go new file mode 100644 index 0000000..55d3f8c --- /dev/null +++ b/common/named_entity.go @@ -0,0 +1,15 @@ +package common + +type NamedEntity struct { + name string +} + +// NewNamedEntity constructor +func NewNamedEntity(name string) NamedEntity { + return NamedEntity{name: name} +} + +// Name getter +func (e NamedEntity) Name() string { + return e.name +} diff --git a/common/named_entity_test.go b/common/named_entity_test.go new file mode 100644 index 0000000..eb94d59 --- /dev/null +++ b/common/named_entity_test.go @@ -0,0 +1,65 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewNamedEntity(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want NamedEntity + }{ + { + name: "empty name is valid", + args: args{ + name: "", + }, + want: NamedEntity{ + name: "", + }, + }, + { + name: "with name", + args: args{ + name: "component1", + }, + want: NamedEntity{ + name: "component1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, NewNamedEntity(tt.args.name)) + }) + } +} + +func TestNamedEntity_Name(t *testing.T) { + tests := []struct { + name string + namedEntity NamedEntity + want string + }{ + { + name: "empty name", + namedEntity: NewNamedEntity(""), + want: "", + }, + { + name: "with name", + namedEntity: NewNamedEntity("port2"), + want: "port2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.namedEntity.Name()) + }) + } +} diff --git a/component/activation_result.go b/component/activation_result.go index 9a64eda..dbdb92a 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -3,22 +3,50 @@ package component import ( "errors" "fmt" + "github.com/hovsep/fmesh/common" ) // ActivationResult defines the result (possibly an error) of the activation of given component in given cycle type ActivationResult struct { - componentName string - activated bool - code ActivationResultCode - err error + *common.Chainable + componentName string + activated bool + code ActivationResultCode + activationError error //Error returned from component activation function } // ActivationResultCode denotes a specific info about how a component been activated or why not activated at all type ActivationResultCode int +func (a ActivationResultCode) String() string { + switch a { + case ActivationCodeUndefined: + return "UNDEFINED" + case ActivationCodeOK: + return "OK" + case ActivationCodeNoInput: + return "No input" + case ActivationCodeNoFunction: + return "Activation function is missing" + case ActivationCodeReturnedError: + return "Returned error" + case ActivationCodePanicked: + return "Panicked" + case ActivationCodeWaitingForInputsClear: + return "Component is waiting for input" + case ActivationCodeWaitingForInputsKeep: + return "Component is waiting for input and wants to keep all inputs till next cycle" + default: + return "Unsupported code" + } +} + const ( + // ActivationCodeUndefined : used for error handling as zero instance + ActivationCodeUndefined ActivationResultCode = iota + // ActivationCodeOK : component is activated and did not return any errors - ActivationCodeOK ActivationResultCode = iota + ActivationCodeOK // ActivationCodeNoInput : component is not activated because it has no input set ActivationCodeNoInput @@ -32,7 +60,7 @@ const ( // ActivationCodePanicked : component is activated, but panicked ActivationCodePanicked - // ActivationCodeWaitingForInputs : component waits for specific inputs, but all input signals in current activation cycle may be cleared (default behaviour) + // ActivationCodeWaitingForInputsClear : component waits for specific inputs, but all input signals in current activation cycle may be cleared (default behaviour) ActivationCodeWaitingForInputsClear // ActivationCodeWaitingForInputsKeep : component waits for specific inputs, but wants to keep current input signals for the next cycle @@ -44,6 +72,7 @@ const ( func NewActivationResult(componentName string) *ActivationResult { return &ActivationResult{ componentName: componentName, + Chainable: common.NewChainable(), } } @@ -57,9 +86,9 @@ func (ar *ActivationResult) Activated() bool { return ar.activated } -// Error getter -func (ar *ActivationResult) Error() error { - return ar.err +// ActivationError getter +func (ar *ActivationResult) ActivationError() error { + return ar.activationError } // Code getter @@ -69,12 +98,12 @@ func (ar *ActivationResult) Code() ActivationResultCode { // IsError returns true when activation result has an error func (ar *ActivationResult) IsError() bool { - return ar.code == ActivationCodeReturnedError && ar.Error() != nil + return ar.code == ActivationCodeReturnedError && ar.ActivationError() != nil } // IsPanic returns true when activation result is derived from panic func (ar *ActivationResult) IsPanic() bool { - return ar.code == ActivationCodePanicked && ar.Error() != nil + return ar.code == ActivationCodePanicked && ar.ActivationError() != nil } // SetActivated setter @@ -89,9 +118,9 @@ func (ar *ActivationResult) WithActivationCode(code ActivationResultCode) *Activ return ar } -// WithError setter -func (ar *ActivationResult) WithError(err error) *ActivationResult { - ar.err = err +// WithActivationError sets the activation result error +func (ar *ActivationResult) WithActivationError(activationError error) *ActivationResult { + ar.activationError = activationError return ar } @@ -122,7 +151,7 @@ func (c *Component) newActivationResultReturnedError(err error) *ActivationResul return NewActivationResult(c.Name()). SetActivated(true). WithActivationCode(ActivationCodeReturnedError). - WithError(fmt.Errorf("component returned an error: %w", err)) + WithActivationError(fmt.Errorf("component returned an error: %w", err)) } // newActivationResultPanicked builds a specific activation result @@ -130,7 +159,7 @@ func (c *Component) newActivationResultPanicked(err error) *ActivationResult { return NewActivationResult(c.Name()). SetActivated(true). WithActivationCode(ActivationCodePanicked). - WithError(err) + WithActivationError(err) } func (c *Component) newActivationResultWaitingForInputs(err error) *ActivationResult { @@ -141,7 +170,7 @@ func (c *Component) newActivationResultWaitingForInputs(err error) *ActivationRe return NewActivationResult(c.Name()). SetActivated(true). WithActivationCode(activationCode). - WithError(err) + WithActivationError(err) } func IsWaitingForInput(activationResult *ActivationResult) bool { @@ -152,3 +181,9 @@ func IsWaitingForInput(activationResult *ActivationResult) bool { func WantsToKeepInputs(activationResult *ActivationResult) bool { return activationResult.Code() == ActivationCodeWaitingForInputsKeep } + +// WithErr returns activation result with chain error +func (ar *ActivationResult) WithErr(err error) *ActivationResult { + ar.SetErr(err) + return ar +} diff --git a/component/collection.go b/component/collection.go index af30267..0c52c32 100644 --- a/component/collection.go +++ b/component/collection.go @@ -1,22 +1,74 @@ package component +import ( + "fmt" + "github.com/hovsep/fmesh/common" +) + +// @TODO: make type unexported +type ComponentsMap map[string]*Component + // Collection is a collection of components with useful methods -type Collection map[string]*Component +type Collection struct { + *common.Chainable + components ComponentsMap +} // NewCollection creates empty collection -func NewCollection() Collection { - return make(Collection) +func NewCollection() *Collection { + return &Collection{ + Chainable: common.NewChainable(), + components: make(ComponentsMap), + } } // ByName returns a component by its name -func (collection Collection) ByName(name string) *Component { - return collection[name] +func (c *Collection) ByName(name string) *Component { + if c.HasErr() { + return New("").WithErr(c.Err()) + } + + component, ok := c.components[name] + + if !ok { + c.SetErr(fmt.Errorf("%w, component name: %s", errNotFound, name)) + return New("").WithErr(c.Err()) + } + + return component } // With adds components and returns the collection -func (collection Collection) With(components ...*Component) Collection { +func (c *Collection) With(components ...*Component) *Collection { + if c.HasErr() { + return c + } + for _, component := range components { - collection[component.Name()] = component + c.components[component.Name()] = component + + if component.HasErr() { + return c.WithErr(component.Err()) + } + } + + return c +} + +// WithErr returns group with error +func (c *Collection) WithErr(err error) *Collection { + c.SetErr(err) + return c +} + +// Len returns number of ports in collection +func (c *Collection) Len() int { + return len(c.components) +} + +func (c *Collection) Components() (ComponentsMap, error) { + if c.HasErr() { + return nil, c.Err() } - return collection + return c.components, nil } diff --git a/component/collection_test.go b/component/collection_test.go index 306b53d..ec52f9f 100644 --- a/component/collection_test.go +++ b/component/collection_test.go @@ -1,7 +1,7 @@ package component import ( - "github.com/hovsep/fmesh/port" + "fmt" "github.com/stretchr/testify/assert" "testing" ) @@ -12,7 +12,7 @@ func TestCollection_ByName(t *testing.T) { } tests := []struct { name string - components Collection + components *Collection args args want *Component }{ @@ -22,13 +22,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "c2", }, - want: &Component{ - name: "c2", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, - }, + want: New("c2"), }, { name: "component not found", @@ -36,7 +30,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "c3", }, - want: nil, + want: New("").WithErr(fmt.Errorf("%w, component name: %s", errNotFound, "c3")), }, } for _, tt := range tests { @@ -52,9 +46,9 @@ func TestCollection_With(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "adding nothing to empty collection", @@ -62,8 +56,8 @@ func TestCollection_With(t *testing.T) { args: args{ components: nil, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { @@ -72,11 +66,10 @@ func TestCollection_With(t *testing.T) { args: args{ components: []*Component{New("c1"), New("c2")}, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 2) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, 2, collection.Len()) assert.NotNil(t, collection.ByName("c1")) assert.NotNil(t, collection.ByName("c2")) - assert.Nil(t, collection.ByName("c999")) }, }, { @@ -85,13 +78,12 @@ func TestCollection_With(t *testing.T) { args: args{ components: []*Component{New("c3"), New("c4")}, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 4) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, 4, collection.Len()) assert.NotNil(t, collection.ByName("c1")) assert.NotNil(t, collection.ByName("c2")) assert.NotNil(t, collection.ByName("c3")) assert.NotNil(t, collection.ByName("c4")) - assert.Nil(t, collection.ByName("c999")) }, }, } diff --git a/component/component.go b/component/component.go index 2161d70..7828040 100644 --- a/component/component.go +++ b/component/component.go @@ -3,93 +3,231 @@ package component import ( "errors" "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/port" ) -type ActivationFunc func(inputs port.Collection, outputs port.Collection) error +type ActivationFunc func(inputs *port.Collection, outputs *port.Collection) error // Component defines a main building block of FMesh type Component struct { - name string - description string - inputs port.Collection - outputs port.Collection - f ActivationFunc + common.NamedEntity + common.DescribedEntity + common.LabeledEntity + *common.Chainable + inputs *port.Collection + outputs *port.Collection + f ActivationFunc } // New creates initialized component func New(name string) *Component { return &Component{ - name: name, - inputs: port.NewCollection(), - outputs: port.NewCollection(), + NamedEntity: common.NewNamedEntity(name), + DescribedEntity: common.NewDescribedEntity(""), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), } } // WithDescription sets a description func (c *Component) WithDescription(description string) *Component { - c.description = description + if c.HasErr() { + return c + } + + c.DescribedEntity = common.NewDescribedEntity(description) + return c +} + +// withInputPorts sets input ports collection +func (c *Component) withInputPorts(collection *port.Collection) *Component { + if c.HasErr() { + return c + } + if collection.HasErr() { + return c.WithErr(collection.Err()) + } + c.inputs = collection + return c +} + +// withOutputPorts sets input ports collection +func (c *Component) withOutputPorts(collection *port.Collection) *Component { + if c.HasErr() { + return c + } + if collection.HasErr() { + return c.WithErr(collection.Err()) + } + + c.outputs = collection return c } // WithInputs ads input ports func (c *Component) WithInputs(portNames ...string) *Component { - c.inputs = c.Inputs().With(port.NewGroup(portNames...)...) - return c + if c.HasErr() { + return c + } + + ports, err := port.NewGroup(portNames...).Ports() + if err != nil { + c.SetErr(err) + return New("").WithErr(c.Err()) + } + + return c.withInputPorts(c.Inputs().With(ports...)) } // WithOutputs adds output ports func (c *Component) WithOutputs(portNames ...string) *Component { - c.outputs = c.Outputs().With(port.NewGroup(portNames...)...) - return c + if c.HasErr() { + return c + } + + ports, err := port.NewGroup(portNames...).Ports() + if err != nil { + c.SetErr(err) + return New("").WithErr(c.Err()) + } + return c.withOutputPorts(c.Outputs().With(ports...)) } // WithInputsIndexed creates multiple prefixed ports func (c *Component) WithInputsIndexed(prefix string, startIndex int, endIndex int) *Component { - c.inputs = c.Inputs().WithIndexed(prefix, startIndex, endIndex) - return c + if c.HasErr() { + return c + } + + return c.withInputPorts(c.Inputs().WithIndexed(prefix, startIndex, endIndex)) } // WithOutputsIndexed creates multiple prefixed ports func (c *Component) WithOutputsIndexed(prefix string, startIndex int, endIndex int) *Component { - c.outputs = c.Outputs().WithIndexed(prefix, startIndex, endIndex) - return c + if c.HasErr() { + return c + } + + return c.withOutputPorts(c.Outputs().WithIndexed(prefix, startIndex, endIndex)) } // WithActivationFunc sets activation function func (c *Component) WithActivationFunc(f ActivationFunc) *Component { + if c.HasErr() { + return c + } + c.f = f return c } -// Name getter -func (c *Component) Name() string { - return c.name -} - -// Description getter -func (c *Component) Description() string { - return c.description +// WithLabels sets labels and returns the component +func (c *Component) WithLabels(labels common.LabelsCollection) *Component { + if c.HasErr() { + return c + } + c.LabeledEntity.SetLabels(labels) + return c } // Inputs getter -func (c *Component) Inputs() port.Collection { +func (c *Component) Inputs() *port.Collection { + if c.HasErr() { + return port.NewCollection().WithErr(c.Err()) + } + return c.inputs } // Outputs getter -func (c *Component) Outputs() port.Collection { +func (c *Component) Outputs() *port.Collection { + if c.HasErr() { + return port.NewCollection().WithErr(c.Err()) + } + return c.outputs } +// OutputByName is shortcut method +func (c *Component) OutputByName(name string) *port.Port { + if c.HasErr() { + return port.New("").WithErr(c.Err()) + } + outputPort := c.Outputs().ByName(name) + if outputPort.HasErr() { + c.SetErr(outputPort.Err()) + return port.New("").WithErr(c.Err()) + } + return outputPort +} + +// InputByName is shortcut method +func (c *Component) InputByName(name string) *port.Port { + if c.HasErr() { + return port.New("").WithErr(c.Err()) + } + inputPort := c.Inputs().ByName(name) + if inputPort.HasErr() { + c.SetErr(inputPort.Err()) + return port.New("").WithErr(c.Err()) + } + return inputPort +} + // hasActivationFunction checks when activation function is set func (c *Component) hasActivationFunction() bool { + if c.HasErr() { + return false + } + return c.f != nil } +// propagateChainErrors propagates up all chain errors that might have not been propagated yet +func (c *Component) propagateChainErrors() { + if c.Inputs().HasErr() { + c.SetErr(c.Inputs().Err()) + return + } + + if c.Outputs().HasErr() { + c.SetErr(c.Outputs().Err()) + return + } + + for _, p := range c.Inputs().PortsOrNil() { + if p.HasErr() { + c.SetErr(p.Err()) + return + } + } + + for _, p := range c.Outputs().PortsOrNil() { + if p.HasErr() { + c.SetErr(p.Err()) + return + } + } +} + // MaybeActivate tries to run the activation function if all required conditions are met // @TODO: hide this method from user +// @TODO: can we remove named return ? func (c *Component) MaybeActivate() (activationResult *ActivationResult) { + c.propagateChainErrors() + + if c.HasErr() { + activationResult = NewActivationResult(c.Name()).WithErr(c.Err()) + return + } + defer func() { if r := recover(); r != nil { activationResult = c.newActivationResultPanicked(fmt.Errorf("panicked with: %v", r)) @@ -102,7 +240,7 @@ func (c *Component) MaybeActivate() (activationResult *ActivationResult) { return } - if !c.inputs.AnyHasSignals() { + if !c.Inputs().AnyHasSignals() { //No inputs set, stop here activationResult = c.newActivationResultNoInput() return @@ -126,13 +264,36 @@ func (c *Component) MaybeActivate() (activationResult *ActivationResult) { } // FlushOutputs pushed signals out of the component outputs to pipes and clears outputs -func (c *Component) FlushOutputs() { - for _, out := range c.outputs { - out.Flush() +func (c *Component) FlushOutputs() *Component { + if c.HasErr() { + return c } + + ports, err := c.Outputs().Ports() + if err != nil { + c.SetErr(err) + return New("").WithErr(c.Err()) + } + for _, out := range ports { + out = out.Flush() + if out.HasErr() { + return c.WithErr(out.Err()) + } + } + return c } // ClearInputs clears all input ports -func (c *Component) ClearInputs() { +func (c *Component) ClearInputs() *Component { + if c.HasErr() { + return c + } c.Inputs().Clear() + return c +} + +// WithErr returns component with error +func (c *Component) WithErr(err error) *Component { + c.SetErr(err) + return c } diff --git a/component/component_test.go b/component/component_test.go index 0c8da6a..b7c4e22 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -2,6 +2,7 @@ package component import ( "errors" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/port" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -22,26 +23,14 @@ func TestNewComponent(t *testing.T) { args: args{ name: "", }, - want: &Component{ - name: "", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, - }, + want: New(""), }, { name: "with name", args: args{ name: "multiplier", }, - want: &Component{ - name: "multiplier", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, - }, + want: New("multiplier"), }, } for _, tt := range tests { @@ -51,105 +40,73 @@ func TestNewComponent(t *testing.T) { } } -func TestComponent_Name(t *testing.T) { - tests := []struct { - name string - component *Component - want string - }{ - { - name: "empty name", - component: New(""), - want: "", - }, - { - name: "with name", - component: New("c1"), - want: "c1", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.component.Name()) - }) - } -} - -func TestComponent_Description(t *testing.T) { - tests := []struct { - name string - component *Component - want string - }{ - { - name: "no description", - component: New("c1"), - want: "", - }, - { - name: "with description", - component: New("c1").WithDescription("descr"), - want: "descr", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.component.Description()) - }) - } -} - func TestComponent_FlushOutputs(t *testing.T) { - sink := port.New("sink") - - componentWithNoOutputs := New("c1") - componentWithCleanOutputs := New("c1").WithOutputs("o1", "o2") - - componentWithAllOutputsSet := New("c1").WithOutputs("o1", "o2") - componentWithAllOutputsSet.Outputs().ByNames("o1").PutSignals(signal.New(777)) - componentWithAllOutputsSet.Outputs().ByNames("o2").PutSignals(signal.New(888)) - componentWithAllOutputsSet.Outputs().ByNames("o1", "o2").PipeTo(sink) - tests := []struct { - name string - component *Component - destPort *port.Port //Where the component flushes ALL it's inputs - assertions func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) + name string + getComponent func() *Component + assertions func(t *testing.T, componentAfterFlush *Component) }{ { - name: "no outputs", - component: componentWithNoOutputs, - destPort: nil, - assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { + name: "no outputs", + getComponent: func() *Component { + return New("c1") + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { assert.NotNil(t, componentAfterFlush.Outputs()) - assert.Empty(t, componentAfterFlush.Outputs()) + assert.Zero(t, componentAfterFlush.Outputs().Len()) }, }, { - name: "output has no signal set", - component: componentWithCleanOutputs, - destPort: nil, - assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { + name: "output has no signal set", + getComponent: func() *Component { + return New("c1").WithOutputs("o1", "o2") + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { assert.False(t, componentAfterFlush.Outputs().AnyHasSignals()) }, }, { - name: "happy path", - component: componentWithAllOutputsSet, - destPort: sink, - assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { - assert.Contains(t, destPort.Signals().AllPayloads(), 777) - assert.Contains(t, destPort.Signals().AllPayloads(), 888) - assert.Len(t, destPort.Signals().AllPayloads(), 2) - // Signals are disposed when port is flushed + name: "happy path", + getComponent: func() *Component { + sink := port.New("sink").WithLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }) + c := New("c1").WithOutputs("o1", "o2") + c.Outputs().ByNames("o1").PutSignals(signal.New(777)) + c.Outputs().ByNames("o2").PutSignals(signal.New(888)) + c.Outputs().ByNames("o1", "o2").PipeTo(sink) + return c + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { + destPort := componentAfterFlush.OutputByName("o1").Pipes().PortsOrNil()[0] + allPayloads, err := destPort.AllSignalsPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 777) + assert.Contains(t, allPayloads, 888) + assert.Len(t, allPayloads, 2) + // Buffer is cleared when port is flushed assert.False(t, componentAfterFlush.Outputs().AnyHasSignals()) }, }, + { + name: "with chain error", + getComponent: func() *Component { + sink := port.New("sink") + c := New("c").WithOutputs("o1").WithErr(errors.New("some error")) + //Lines below are ignored as error immediately propagates up to component level + c.Outputs().ByName("o1").PipeTo(sink) + c.Outputs().ByName("o1").PutSignals(signal.New("signal from component with chain error")) + return c + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { + assert.False(t, componentAfterFlush.OutputByName("o1").HasPipes()) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.component.FlushOutputs() - tt.assertions(t, tt.component, tt.destPort) + componentAfter := tt.getComponent().FlushOutputs() + tt.assertions(t, componentAfter) }) } } @@ -158,20 +115,21 @@ func TestComponent_Inputs(t *testing.T) { tests := []struct { name string component *Component - want port.Collection + want *port.Collection }{ { name: "no inputs", component: New("c1"), - want: port.Collection{}, + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), }, { name: "with inputs", component: New("c1").WithInputs("i1", "i2"), - want: port.Collection{ - "i1": port.New("i1"), - "i2": port.New("i2"), - }, + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }).With(port.New("i1"), port.New("i2")), }, } for _, tt := range tests { @@ -185,20 +143,21 @@ func TestComponent_Outputs(t *testing.T) { tests := []struct { name string component *Component - want port.Collection + want *port.Collection }{ { name: "no outputs", component: New("c1"), - want: port.Collection{}, + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), }, { name: "with outputs", component: New("c1").WithOutputs("o1", "o2"), - want: port.Collection{ - "o1": port.New("o1"), - "o2": port.New("o2"), - }, + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }).With(port.New("o1"), port.New("o2")), }, } for _, tt := range tests { @@ -221,7 +180,7 @@ func TestComponent_WithActivationFunc(t *testing.T) { name: "happy path", component: New("c1"), args: args{ - f: func(inputs port.Collection, outputs port.Collection) error { + f: func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("out1").PutSignals(signal.New(23)) return nil }, @@ -233,17 +192,17 @@ func TestComponent_WithActivationFunc(t *testing.T) { componentAfter := tt.component.WithActivationFunc(tt.args.f) //Compare activation functions by they result and error - testInputs1 := port.NewCollection().With(port.NewGroup("in1", "in2")...) - testInputs2 := port.NewCollection().With(port.NewGroup("in1", "in2")...) - testOutputs1 := port.NewCollection().With(port.NewGroup("out1", "out2")...) - testOutputs2 := port.NewCollection().With(port.NewGroup("out1", "out2")...) + testInputs1 := port.NewCollection().With(port.NewGroup("in1", "in2").PortsOrNil()...) + testInputs2 := port.NewCollection().With(port.NewGroup("in1", "in2").PortsOrNil()...) + testOutputs1 := port.NewCollection().With(port.NewGroup("out1", "out2").PortsOrNil()...) + testOutputs2 := port.NewCollection().With(port.NewGroup("out1", "out2").PortsOrNil()...) err1 := componentAfter.f(testInputs1, testOutputs1) err2 := tt.args.f(testInputs2, testOutputs2) assert.Equal(t, err1, err2) //Compare signals without keys (because they are random) - assert.ElementsMatch(t, testOutputs1.ByName("out1").Signals(), testOutputs2.ByName("out1").Signals()) - assert.ElementsMatch(t, testOutputs1.ByName("out2").Signals(), testOutputs2.ByName("out2").Signals()) + assert.ElementsMatch(t, testOutputs1.ByName("out1").AllSignalsOrNil(), testOutputs2.ByName("out1").AllSignalsOrNil()) + assert.ElementsMatch(t, testOutputs1.ByName("out2").AllSignalsOrNil(), testOutputs2.ByName("out2").AllSignalsOrNil()) }) } @@ -266,11 +225,17 @@ func TestComponent_WithDescription(t *testing.T) { description: "descr", }, want: &Component{ - name: "c1", - description: "descr", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity("descr"), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, } @@ -298,14 +263,17 @@ func TestComponent_WithInputs(t *testing.T) { portNames: []string{"p1", "p2"}, }, want: &Component{ - name: "c1", - description: "", - inputs: port.Collection{ - "p1": port.New("p1"), - "p2": port.New("p2"), - }, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }).With(port.New("p1"), port.New("p2")), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, { @@ -315,11 +283,17 @@ func TestComponent_WithInputs(t *testing.T) { portNames: nil, }, want: &Component{ - name: "c1", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, } @@ -347,13 +321,16 @@ func TestComponent_WithOutputs(t *testing.T) { portNames: []string{"p1", "p2"}, }, want: &Component{ - name: "c1", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{ - "p1": port.New("p1"), - "p2": port.New("p2"), - }, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }).With(port.New("p1"), port.New("p2")), f: nil, }, }, @@ -364,11 +341,17 @@ func TestComponent_WithOutputs(t *testing.T) { portNames: nil, }, want: &Component{ - name: "c1", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, } @@ -396,7 +379,7 @@ func TestComponent_MaybeActivate(t *testing.T) { name: "component with inputs set, but no activation func", getComponent: func() *Component { c := New("c1").WithInputs("i1") - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -409,9 +392,8 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) - return nil + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) }) return c }, @@ -424,17 +406,17 @@ func TestComponent_MaybeActivate(t *testing.T) { getComponent: func() *Component { c := New("c1"). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("test error") }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodeReturnedError). - WithError(errors.New("component returned an error: test error")), + WithActivationError(errors.New("component returned an error: test error")), }, { name: "activated without error", @@ -442,12 +424,11 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) - return nil + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -460,19 +441,18 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { panic(errors.New("oh shrimps")) return nil }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodePanicked). - WithError(errors.New("panicked with: oh shrimps")), + WithActivationError(errors.New("panicked with: oh shrimps")), }, { name: "component panicked with string", @@ -480,19 +460,90 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { panic("oh shrimps") return nil }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodePanicked). - WithError(errors.New("panicked with: oh shrimps")), + WithActivationError(errors.New("panicked with: oh shrimps")), + }, + { + name: "component is waiting for inputs", + getComponent: func() *Component { + c1 := New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + if !inputs.ByNames("i1", "i2").AllHaveSignals() { + return NewErrWaitForInputs(false) + } + return nil + }) + + // Only one input set + c1.InputByName("i1").PutSignals(signal.New(123)) + + return c1 + }, + wantActivationResult: &ActivationResult{ + componentName: "c1", + activated: true, + code: ActivationCodeWaitingForInputsClear, + activationError: NewErrWaitForInputs(false), + }, + }, + { + name: "component is waiting for inputs and wants to keep them", + getComponent: func() *Component { + c1 := New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + if !inputs.ByNames("i1", "i2").AllHaveSignals() { + return NewErrWaitForInputs(true) + } + return nil + }) + + // Only one input set + c1.InputByName("i1").PutSignals(signal.New(123)) + + return c1 + }, + wantActivationResult: &ActivationResult{ + componentName: "c1", + activated: true, + code: ActivationCodeWaitingForInputsKeep, + activationError: NewErrWaitForInputs(true), + }, + }, + { + name: "with chain error from input port", + getComponent: func() *Component { + c := New("c").WithInputs("i1").WithOutputs("o1") + c.Inputs().With(port.New("p").WithErr(errors.New("some error"))) + return c + }, + wantActivationResult: NewActivationResult("c"). + WithActivationCode(ActivationCodeUndefined). + WithErr(errors.New("some error")), + }, + { + name: "with chain error from output port", + getComponent: func() *Component { + c := New("c").WithInputs("i1").WithOutputs("o1") + c.Outputs().With(port.New("p").WithErr(errors.New("some error"))) + return c + }, + wantActivationResult: NewActivationResult("c"). + WithActivationCode(ActivationCodeUndefined). + WithErr(errors.New("some error")), }, } for _, tt := range tests { @@ -502,7 +553,7 @@ func TestComponent_MaybeActivate(t *testing.T) { assert.Equal(t, tt.wantActivationResult.ComponentName(), gotActivationResult.ComponentName()) assert.Equal(t, tt.wantActivationResult.Code(), gotActivationResult.Code()) if tt.wantActivationResult.IsError() { - assert.EqualError(t, gotActivationResult.Error(), tt.wantActivationResult.Error().Error()) + assert.EqualError(t, gotActivationResult.ActivationError(), tt.wantActivationResult.ActivationError().Error()) } else { assert.False(t, gotActivationResult.IsError()) } @@ -532,8 +583,8 @@ func TestComponent_WithInputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Outputs(), 2) - assert.Len(t, component.Inputs(), 3) + assert.Equal(t, component.Outputs().Len(), 2) + assert.Equal(t, component.Inputs().Len(), 3) }, }, { @@ -545,8 +596,8 @@ func TestComponent_WithInputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Outputs(), 2) - assert.Len(t, component.Inputs(), 5) + assert.Equal(t, component.Outputs().Len(), 2) + assert.Equal(t, component.Inputs().Len(), 5) }, }, } @@ -581,8 +632,8 @@ func TestComponent_WithOutputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Inputs(), 2) - assert.Len(t, component.Outputs(), 3) + assert.Equal(t, component.Inputs().Len(), 2) + assert.Equal(t, component.Outputs().Len(), 3) }, }, { @@ -594,8 +645,8 @@ func TestComponent_WithOutputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Inputs(), 2) - assert.Len(t, component.Outputs(), 5) + assert.Equal(t, component.Inputs().Len(), 2) + assert.Equal(t, component.Outputs().Len(), 5) }, }, } @@ -608,3 +659,98 @@ func TestComponent_WithOutputsIndexed(t *testing.T) { }) } } + +func TestComponent_WithLabels(t *testing.T) { + type args struct { + labels common.LabelsCollection + } + tests := []struct { + name string + component *Component + args args + assertions func(t *testing.T, component *Component) + }{ + { + name: "happy path", + component: New("c1"), + args: args{ + labels: common.LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, + }, + assertions: func(t *testing.T, component *Component) { + assert.Len(t, component.Labels(), 2) + assert.True(t, component.HasAllLabels("l1", "l2")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + componentAfter := tt.component.WithLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, componentAfter) + } + }) + } +} + +func TestComponent_ShortcutMethods(t *testing.T) { + t.Run("InputByName", func(t *testing.T) { + c := New("c").WithInputs("a", "b", "c") + assert.Equal(t, port.New("b").WithLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), c.InputByName("b")) + }) + + t.Run("OutputByName", func(t *testing.T) { + c := New("c").WithOutputs("a", "b", "c") + assert.Equal(t, port.New("b").WithLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), c.OutputByName("b")) + }) +} + +func TestComponent_ClearInputs(t *testing.T) { + tests := []struct { + name string + getComponent func() *Component + assertions func(t *testing.T, componentAfter *Component) + }{ + { + name: "no side effects", + getComponent: func() *Component { + return New("c").WithInputs("i1").WithOutputs("o1") + }, + assertions: func(t *testing.T, componentAfter *Component) { + assert.Equal(t, 1, componentAfter.Inputs().Len()) + assert.Equal(t, 1, componentAfter.Outputs().Len()) + assert.False(t, componentAfter.Inputs().AnyHasSignals()) + assert.False(t, componentAfter.Outputs().AnyHasSignals()) + }, + }, + { + name: "only inputs are cleared", + getComponent: func() *Component { + c := New("c").WithInputs("i1").WithOutputs("o1") + c.Inputs().ByName("i1").PutSignals(signal.New(10)) + c.Outputs().ByName("o1").PutSignals(signal.New(20)) + return c + }, + assertions: func(t *testing.T, componentAfter *Component) { + assert.Equal(t, 1, componentAfter.Inputs().Len()) + assert.Equal(t, 1, componentAfter.Outputs().Len()) + assert.False(t, componentAfter.Inputs().AnyHasSignals()) + assert.True(t, componentAfter.Outputs().ByName("o1").HasSignals()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + componentAfter := tt.getComponent().ClearInputs() + if tt.assertions != nil { + tt.assertions(t, componentAfter) + } + }) + } +} diff --git a/component/errors.go b/component/errors.go index 671df5f..9d2d808 100644 --- a/component/errors.go +++ b/component/errors.go @@ -6,6 +6,7 @@ import ( ) var ( + errNotFound = errors.New("component not found") errWaitingForInputs = errors.New("component is waiting for some inputs") errWaitingForInputsKeep = fmt.Errorf("%w: do not clear input ports", errWaitingForInputs) ) diff --git a/cycle/collection.go b/cycle/collection.go deleted file mode 100644 index 1008d8f..0000000 --- a/cycle/collection.go +++ /dev/null @@ -1,19 +0,0 @@ -package cycle - -// Collection contains the results of several activation cycles -type Collection []*Cycle - -// NewCollection creates a collection -func NewCollection() Collection { - return make(Collection, 0) -} - -// With adds cycle results to existing collection -func (collection Collection) With(cycleResults ...*Cycle) Collection { - newCollection := make(Collection, len(collection)+len(cycleResults)) - copy(newCollection, collection) - for i, cycleResult := range cycleResults { - newCollection[len(collection)+i] = cycleResult - } - return newCollection -} diff --git a/cycle/collection_test.go b/cycle/collection_test.go deleted file mode 100644 index 2f4f580..0000000 --- a/cycle/collection_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package cycle - -import ( - "github.com/hovsep/fmesh/component" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestNewCollection(t *testing.T) { - tests := []struct { - name string - want Collection - }{ - { - name: "happy path", - want: Collection{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, NewCollection()) - }) - } -} - -func TestCollection_Add(t *testing.T) { - type args struct { - cycleResults []*Cycle - } - tests := []struct { - name string - cycleResults Collection - args args - want Collection - }{ - { - name: "happy path", - cycleResults: NewCollection(), - args: args{ - cycleResults: []*Cycle{ - New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false)), - New().WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), - }, - }, - want: Collection{ - { - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(false), - }, - }, - { - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(true), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.cycleResults.With(tt.args.cycleResults...)) - }) - } -} diff --git a/cycle/cycle.go b/cycle/cycle.go index 72b8118..7051b76 100644 --- a/cycle/cycle.go +++ b/cycle/cycle.go @@ -1,19 +1,23 @@ package cycle import ( + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "sync" ) -// Cycle contains the info about given activation cycle +// Cycle contains the info about one activation cycle type Cycle struct { sync.Mutex + *common.Chainable + number int activationResults component.ActivationResultCollection } // New creates a new cycle func New() *Cycle { return &Cycle{ + Chainable: common.NewChainable(), activationResults: component.NewActivationResultCollection(), } } @@ -43,3 +47,20 @@ func (cycle *Cycle) WithActivationResults(activationResults ...*component.Activa cycle.activationResults = cycle.ActivationResults().Add(activationResults...) return cycle } + +// Number returns sequence number +func (cycle *Cycle) Number() int { + return cycle.number +} + +// WithNumber sets the sequence number +func (cycle *Cycle) WithNumber(number int) *Cycle { + cycle.number = number + return cycle +} + +// WithErr returns cycle with error +func (cycle *Cycle) WithErr(err error) *Cycle { + cycle.SetErr(err) + return cycle +} diff --git a/cycle/cycle_test.go b/cycle/cycle_test.go index 88e3f91..6d6d82f 100644 --- a/cycle/cycle_test.go +++ b/cycle/cycle_test.go @@ -8,22 +8,11 @@ import ( ) func TestNew(t *testing.T) { - tests := []struct { - name string - want *Cycle - }{ - { - name: "happy path", - want: &Cycle{ - activationResults: component.ActivationResultCollection{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, New()) - }) - } + t.Run("happy path", func(t *testing.T) { + cycle := New() + assert.NotNil(t, cycle) + assert.False(t, cycle.HasErr()) + }) } func TestCycle_ActivationResults(t *testing.T) { @@ -113,7 +102,7 @@ func TestCycle_HasErrors(t *testing.T) { name: "some components returned errors", cycleResult: New().WithActivationResults( component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithActivationError(errors.New("some error")), component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), ), want: true, @@ -143,7 +132,7 @@ func TestCycle_HasPanics(t *testing.T) { component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), component.NewActivationResult("c2").SetActivated(false).WithActivationCode(component.ActivationCodeNoFunction), component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithActivationError(errors.New("some error")), ), want: false, }, @@ -151,9 +140,9 @@ func TestCycle_HasPanics(t *testing.T) { name: "some components panicked", cycleResult: New().WithActivationResults( component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithActivationError(errors.New("some error")), component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked).WithError(errors.New("some panic")), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked).WithActivationError(errors.New("some panic")), ), want: true, }, @@ -170,10 +159,10 @@ func TestCycle_WithActivationResults(t *testing.T) { activationResults []*component.ActivationResult } tests := []struct { - name string - cycleResult *Cycle - args args - want *Cycle + name string + cycleResult *Cycle + args args + wantActivationResults component.ActivationResultCollection }{ { name: "nothing added", @@ -181,7 +170,7 @@ func TestCycle_WithActivationResults(t *testing.T) { args: args{ activationResults: nil, }, - want: New(), + wantActivationResults: component.NewActivationResultCollection(), }, { name: "adding to empty collection", @@ -192,11 +181,9 @@ func TestCycle_WithActivationResults(t *testing.T) { component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), }, }, - want: &Cycle{ - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), - }, + wantActivationResults: component.ActivationResultCollection{ + "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), }, }, { @@ -215,19 +202,17 @@ func TestCycle_WithActivationResults(t *testing.T) { component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), }, }, - want: &Cycle{ - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), - "c3": component.NewActivationResult("c3").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError), - "c4": component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), - }, + wantActivationResults: component.ActivationResultCollection{ + "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + "c3": component.NewActivationResult("c3").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError), + "c4": component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.cycleResult.WithActivationResults(tt.args.activationResults...)) + assert.Equal(t, tt.wantActivationResults, tt.cycleResult.WithActivationResults(tt.args.activationResults...).ActivationResults()) }) } } diff --git a/cycle/errors.go b/cycle/errors.go new file mode 100644 index 0000000..4559671 --- /dev/null +++ b/cycle/errors.go @@ -0,0 +1,7 @@ +package cycle + +import "errors" + +var ( + errNoCyclesInGroup = errors.New("group has no cycles") +) diff --git a/cycle/group.go b/cycle/group.go new file mode 100644 index 0000000..4e529ed --- /dev/null +++ b/cycle/group.go @@ -0,0 +1,72 @@ +package cycle + +import "github.com/hovsep/fmesh/common" + +// Cycles contains the results of several activation cycles +type Cycles []*Cycle + +type Group struct { + *common.Chainable + cycles Cycles +} + +// NewGroup creates a group of cycles +func NewGroup() *Group { + newGroup := &Group{ + Chainable: common.NewChainable(), + } + cycles := make(Cycles, 0) + return newGroup.withCycles(cycles) +} + +// With adds cycle results to existing collection +func (g *Group) With(cycles ...*Cycle) *Group { + newCycles := make(Cycles, len(g.cycles)+len(cycles)) + copy(newCycles, g.cycles) + for i, c := range cycles { + newCycles[len(g.cycles)+i] = c + } + return g.withCycles(newCycles) +} + +// withSignals sets signals +func (g *Group) withCycles(cycles Cycles) *Group { + g.cycles = cycles + return g +} + +// Cycles getter +func (g *Group) Cycles() (Cycles, error) { + if g.HasErr() { + return nil, g.Err() + } + return g.cycles, nil +} + +// CyclesOrNil returns signals or nil in case of any error +func (g *Group) CyclesOrNil() Cycles { + return g.CyclesOrDefault(nil) +} + +// CyclesOrDefault returns signals or default in case of any error +func (g *Group) CyclesOrDefault(defaultCycles Cycles) Cycles { + signals, err := g.Cycles() + if err != nil { + return defaultCycles + } + return signals +} + +// Len returns number of cycles in group +func (g *Group) Len() int { + return len(g.cycles) +} + +// Last returns the latest cycle added to the group +func (g *Group) Last() *Cycle { + if g.Len() == 0 { + return New().WithErr(errNoCyclesInGroup) + } + + return g.cycles[g.Len()-1] +} diff --git a/cycle/group_test.go b/cycle/group_test.go new file mode 100644 index 0000000..3749038 --- /dev/null +++ b/cycle/group_test.go @@ -0,0 +1,75 @@ +package cycle + +import ( + "github.com/hovsep/fmesh/component" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewGroup(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + group := NewGroup() + assert.NotNil(t, group) + }) +} + +func TestGroup_With(t *testing.T) { + type args struct { + cycles []*Cycle + } + tests := []struct { + name string + group *Group + args args + assertions func(t *testing.T, group *Group) + }{ + { + name: "no addition to empty group", + group: NewGroup(), + args: args{ + cycles: nil, + }, + assertions: func(t *testing.T, group *Group) { + assert.Zero(t, group.Len()) + }, + }, + { + name: "adding nothing to existing group", + group: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), + args: args{ + cycles: nil, + }, + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 1) + }, + }, + { + name: "adding to empty group", + group: NewGroup(), + args: args{ + cycles: []*Cycle{New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))}, + }, + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 1) + }, + }, + { + name: "adding to existing group", + group: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(true))), + args: args{ + cycles: []*Cycle{New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))}, + }, + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 2) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + groupAfter := tt.group.With(tt.args.cycles...) + if tt.assertions != nil { + tt.assertions(t, groupAfter) + } + }) + } +} diff --git a/errors.go b/errors.go index 7f10ce8..dcb20ff 100644 --- a/errors.go +++ b/errors.go @@ -22,4 +22,8 @@ var ( ErrHitAPanic = errors.New("f-mesh hit a panic and will be stopped") ErrUnsupportedErrorHandlingStrategy = errors.New("unsupported error handling strategy") ErrReachedMaxAllowedCycles = errors.New("reached max allowed cycles") + errFailedToRunCycle = errors.New("failed to run cycle") + errNoComponents = errors.New("no components found") + errFailedToClearInputs = errors.New("failed to clear input ports") + ErrFailedToDrain = errors.New("failed to drain") ) diff --git a/examples/async_input/main.go b/examples/async_input/main.go new file mode 100644 index 0000000..c7743ec --- /dev/null +++ b/examples/async_input/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "net/http" + "time" +) + +// This example processes 1 url every 3 seconds +// NOTE: urls are not crawled concurrently, because fm has only 1 worker (crawler component) +func main() { + fm := getMesh() + + urls := []string{ + "http://fffff.com", + "https://google.com", + "http://habr.com", + "http://localhost:80", + "https://postman-echo.com/delay/1", + "https://postman-echo.com/delay/3", + "https://postman-echo.com/delay/5", + "https://postman-echo.com/delay/10", + } + + ticker := time.NewTicker(3 * time.Second) + resultsChan := make(chan []any) + doneChan := make(chan struct{}) // Signals when all urls are processed + + //Producer goroutine + go func() { + for { + select { + case <-ticker.C: + if len(urls) == 0 { + close(resultsChan) + return + } + //Pop an url + url := urls[0] + urls = urls[1:] + + fmt.Println("produce:", url) + + fm.Components().ByName("web crawler").InputByName("url").PutSignals(signal.New(url)) + _, err := fm.Run() + if err != nil { + fmt.Println("fmesh returned error ", err) + } + + if fm.Components().ByName("web crawler").OutputByName("headers").HasSignals() { + results, err := fm.Components().ByName("web crawler").OutputByName("headers").AllSignalsPayloads() + if err != nil { + fmt.Println("Failed to get results ", err) + } + fm.Components().ByName("web crawler").OutputByName("headers").Clear() //@TODO maybe we can add fm.Reset() for cases when FMesh is reused (instead of cleaning ports explicitly) + resultsChan <- results + } + } + } + }() + + //Consumer goroutine + go func() { + for { + select { + case r, ok := <-resultsChan: + if !ok { + fmt.Println("results chan is closed. shutting down the reader") + doneChan <- struct{}{} + return + } + fmt.Println(fmt.Sprintf("consume: %v", r)) + } + } + }() + + <-doneChan +} + +func getMesh() *fmesh.FMesh { + //Setup dependencies + client := &http.Client{} + + //Define components + crawler := component.New("web crawler"). + WithDescription("gets http headers from given url"). + WithInputs("url"). + WithOutputs("errors", "headers").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + if !inputs.ByName("url").HasSignals() { + return component.NewErrWaitForInputs(false) + } + + allUrls, err := inputs.ByName("url").AllSignalsPayloads() + if err != nil { + return err + } + + for _, urlVal := range allUrls { + + url := urlVal.(string) + //All urls will be crawled sequentially + // in order to call them concurrently we need run each request in separate goroutine and handle synchronization (e.g. waitgroup) + response, err := client.Get(url) + if err != nil { + outputs.ByName("errors").PutSignals(signal.New(fmt.Errorf("got error: %w from url: %s", err, url))) + continue + } + + if len(response.Header) == 0 { + outputs.ByName("errors").PutSignals(signal.New(fmt.Errorf("no headers for url %s", url))) + continue + } + + outputs.ByName("headers").PutSignals(signal.New(map[string]http.Header{ + url: response.Header, + })) + } + + return nil + }) + + logger := component.New("error logger"). + WithDescription("logs http errors"). + WithInputs("error").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + if !inputs.ByName("error").HasSignals() { + return component.NewErrWaitForInputs(false) + } + + allErrors, err := inputs.ByName("error").AllSignalsPayloads() + if err != nil { + return err + } + + for _, errVal := range allErrors { + e := errVal.(error) + if e != nil { + fmt.Println("Error logger says:", e) + } + } + + return nil + }) + + //Define pipes + crawler.OutputByName("errors").PipeTo(logger.InputByName("error")) + + return fmesh.New("web scraper").WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + }).WithComponents(crawler, logger) + +} diff --git a/examples/fibonacci/main.go b/examples/fibonacci/main.go new file mode 100644 index 0000000..08de21b --- /dev/null +++ b/examples/fibonacci/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" +) + +// This example demonstrates how a component can have a pipe looped back into its own input, +// enabling a pattern that reactivates the component multiple times. +// By looping the output back into the input, the component can perform repeated calculations +// without explicit looping constructs in the code. +// +// For instance, this approach can be used to calculate Fibonacci numbers without needing +// traditional looping code. Instead, the loop is achieved by configuring ports and pipes, +// where each cycle processes a new Fibonacci term. +func main() { + c1 := component.New("fibonacci number generator"). + WithInputs("i_cur", "i_prev"). + WithOutputs("o_cur", "o_prev"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + cur := inputs.ByName("i_cur").FirstSignalPayloadOrDefault(0).(int) + prev := inputs.ByName("i_prev").FirstSignalPayloadOrDefault(0).(int) + + next := cur + prev + + //Hardcoded limit + if next < 100 { + fmt.Println(next) + outputs.ByName("o_cur").PutSignals(signal.New(next)) + outputs.ByName("o_prev").PutSignals(signal.New(cur)) + } + + return nil + }) + + //Define pipes + c1.Outputs().ByName("o_cur").PipeTo(c1.Inputs().ByName("i_cur")) + c1.Outputs().ByName("o_prev").PipeTo(c1.Inputs().ByName("i_prev")) + + //Build mesh + fm := fmesh.New("fibonacci example").WithComponents(c1) + + //Set inputs (first 2 Fibonacci numbers) + f0, f1 := signal.New(0), signal.New(1) + + c1.Inputs().ByName("i_prev").PutSignals(f0) + c1.Inputs().ByName("i_cur").PutSignals(f1) + + fmt.Println(f0.PayloadOrNil()) + fmt.Println(f1.PayloadOrNil()) + + //Run the mesh + _, err := fm.Run() + + if err != nil { + fmt.Println(err) + } + +} diff --git a/examples/nesting/main.go b/examples/nesting/main.go new file mode 100644 index 0000000..7523fa5 --- /dev/null +++ b/examples/nesting/main.go @@ -0,0 +1,210 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" +) + +type FactorizedNumber struct { + Num int + Factors []any +} + +// This example demonstrates the ability to nest meshes, where a component within a mesh +// can itself be another mesh. This nesting is recursive, allowing for an unlimited depth +// of nested meshes. Each nested mesh behaves as an individual component within the larger +// mesh, enabling complex and hierarchical workflows. +// In this example we implement prime factorization (which is core part of RSA encryption algorithm) as a sub-mesh +func main() { + starter := component.New("starter"). + WithDescription("This component just holds numbers we want to factorize"). + WithInputs("in"). // Single port is enough, as it can hold any number of signals (as long as they fit into1 memory) + WithOutputs("out"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + // Pure bypass + return port.ForwardSignals(inputs.ByName("in"), outputs.ByName("out")) + }) + + filter := component.New("filter"). + WithDescription("In this component we can do some optional filtering"). + WithInputs("in"). + WithOutputs("out", "log"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + isValid := func(num int) bool { + return num < 1000 + } + + for _, sig := range inputs.ByName("in").AllSignalsOrNil() { + if isValid(sig.PayloadOrNil().(int)) { + outputs.ByName("out").PutSignals(sig) + } else { + outputs.ByName("log").PutSignals(sig) + } + } + return nil + }) + + logger := component.New("logger"). + WithDescription("Simple logger"). + WithInputs("in"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + log := func(data any) { + fmt.Printf("LOG: %v", data) + } + + for _, sig := range inputs.ByName("in").AllSignalsOrNil() { + log(sig.PayloadOrNil()) + } + return nil + }) + + factorizer := component.New("factorizer"). + WithDescription("Prime factorization implemented as separate f-mesh"). + WithInputs("in"). + WithOutputs("out"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + //This activation function has no implementation of factorization algorithm, + //it only runs another f-mesh to get results + + //Get nested mesh or meshes + factorization := getPrimeFactorizationMesh() + + // As nested f-mesh processes 1 signal per run we run it in the loop per each number + for _, numSig := range inputs.ByName("in").AllSignalsOrNil() { + //Set init data to nested mesh (pass signals from outer mesh to inner one) + factorization.Components().ByName("starter").InputByName("in").PutSignals(numSig) + + //Run nested mesh + _, err := factorization.Run() + + if err != nil { + return fmt.Errorf("inner mesh failed: %w", err) + } + + // Get results from nested mesh + factors, err := factorization.Components().ByName("results").OutputByName("factors").AllSignalsPayloads() + if err != nil { + return fmt.Errorf("failed to get factors: %w", err) + } + + //Pass results to outer mesh + number := numSig.PayloadOrNil().(int) + outputs.ByName("out").PutSignals(signal.New(FactorizedNumber{ + Num: number, + Factors: factors, + })) + } + + return nil + }) + + //Setup pipes + starter.OutputByName("out").PipeTo(filter.InputByName("in")) + filter.OutputByName("log").PipeTo(logger.InputByName("in")) + filter.OutputByName("out").PipeTo(factorizer.InputByName("in")) + + // Build the mesh + outerMesh := fmesh.New("outer").WithComponents(starter, filter, logger, factorizer) + + //Set init data + outerMesh.Components(). + ByName("starter"). + InputByName("in"). + PutSignals(signal.NewGroup(315).SignalsOrNil()...) + + //Run outer mesh + _, err := outerMesh.Run() + + if err != nil { + fmt.Println(fmt.Errorf("outer mesh failed with error: %w", err)) + } + + //Read results + for _, resSig := range outerMesh.Components().ByName("factorizer").OutputByName("out").AllSignalsOrNil() { + result := resSig.PayloadOrNil().(FactorizedNumber) + fmt.Println(fmt.Sprintf("Factors of number %d : %v", result.Num, result.Factors)) + } +} + +func getPrimeFactorizationMesh() *fmesh.FMesh { + starter := component.New("starter"). + WithDescription("Load the number to be factorized"). + WithInputs("in"). + WithOutputs("out"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + //For simplicity this f-mesh processes only one signal per run, so ignore all except first + outputs.ByName("out").PutSignals(inputs.ByName("in").Buffer().First()) + return nil + }) + + d2 := component.New("d2"). + WithDescription("Divide by smallest prime (2) to handle even factors"). + WithInputs("in"). + WithOutputs("out", "factor"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + number := inputs.ByName("in").FirstSignalPayloadOrNil().(int) + + for number%2 == 0 { + outputs.ByName("factor").PutSignals(signal.New(2)) + number /= 2 + } + + outputs.ByName("out").PutSignals(signal.New(number)) + return nil + }) + + dodd := component.New("dodd"). + WithDescription("Divide by odd primes starting from 3"). + WithInputs("in"). + WithOutputs("out", "factor"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + number := inputs.ByName("in").FirstSignalPayloadOrNil().(int) + divisor := 3 + for number > 1 && divisor*divisor <= number { + for number%divisor == 0 { + outputs.ByName("factor").PutSignals(signal.New(divisor)) + number /= divisor + } + divisor += 2 + } + outputs.ByName("out").PutSignals(signal.New(number)) + return nil + }) + + finalPrime := component.New("final_prime"). + WithDescription("Store the last remaining prime factor, if any"). + WithInputs("in"). + WithOutputs("factor"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + number := inputs.ByName("in").FirstSignalPayloadOrNil().(int) + if number > 1 { + outputs.ByName("factor").PutSignals(signal.New(number)) + } + return nil + }) + + results := component.New("results"). + WithDescription("factors holder"). + WithInputs("factor"). + WithOutputs("factors"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return port.ForwardSignals(inputs.ByName("factor"), outputs.ByName("factors")) + }) + + //Main pipeline starter->d2->dodd->finalPrime + starter.OutputByName("out").PipeTo(d2.InputByName("in")) + d2.OutputByName("out").PipeTo(dodd.InputByName("in")) + dodd.OutputByName("out").PipeTo(finalPrime.InputByName("in")) + + //All found factors are accumulated in results + d2.OutputByName("factor").PipeTo(results.InputByName("factor")) + dodd.OutputByName("factor").PipeTo(results.InputByName("factor")) + finalPrime.OutputByName("factor").PipeTo(results.InputByName("factor")) + + return fmesh.New("prime factors algo"). + WithDescription("Pass single signal to starter"). + WithComponents(starter, d2, dodd, finalPrime, results) +} diff --git a/examples/strings_processing/main.go b/examples/strings_processing/main.go new file mode 100644 index 0000000..539a112 --- /dev/null +++ b/examples/strings_processing/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "os" + "strings" +) + +// This example is used in readme.md +func main() { + fm := fmesh.New("hello world"). + WithComponents( + component.New("concat"). + WithInputs("i1", "i2"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + word1 := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + word2 := inputs.ByName("i2").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(word1 + word2)) + return nil + }), + component.New("case"). + WithInputs("i1"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputString := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(strings.ToTitle(inputString))) + return nil + })). + WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) + + fm.Components().ByName("concat").Outputs().ByName("res").PipeTo( + fm.Components().ByName("case").Inputs().ByName("i1"), + ) + + // Init inputs + fm.Components().ByName("concat").InputByName("i1").PutSignals(signal.New("hello ")) + fm.Components().ByName("concat").InputByName("i2").PutSignals(signal.New("world !")) + + // Run the mesh + _, err := fm.Run() + + // Check for errors + if err != nil { + fmt.Println("F-Mesh returned an error") + os.Exit(1) + } + + //Extract results + results := fm.Components().ByName("case").OutputByName("res").FirstSignalPayloadOrNil() + fmt.Printf("Result is : %v", results) +} diff --git a/experiments/10_shared_pointer/component.go b/experiments/10_shared_pointer/component.go deleted file mode 100644 index 20ca1e8..0000000 --- a/experiments/10_shared_pointer/component.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/10_shared_pointer/errors.go b/experiments/10_shared_pointer/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/10_shared_pointer/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/10_shared_pointer/fmesh.go b/experiments/10_shared_pointer/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/10_shared_pointer/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/10_shared_pointer/go.mod b/experiments/10_shared_pointer/go.mod deleted file mode 100644 index 3a17a50..0000000 --- a/experiments/10_shared_pointer/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/10_shared_pointer - -go 1.23 diff --git a/experiments/10_shared_pointer/main.go b/experiments/10_shared_pointer/main.go deleted file mode 100644 index 686d94b..0000000 --- a/experiments/10_shared_pointer/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "fmt" -) - -// This example demonstrates a multiplexing problem in current implementation (signals are shared pointers when multiplexed) -func main() { - //Define components - gen := &Component{ - name: "number generator", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "num": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - outputs.byName("num").setValue(inputs.byName("i1").getValue()) - return nil - }, - } - - r1 := &Component{ - name: "modifies input signal ", - inputs: Ports{ - "i1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - sig := inputs.byName("i1").getValue() - sig.(*SingleSignal).val = 666 //This modifies the signals for all receivers, as signal is a shared pointer - return nil - }, - } - - r2 := &Component{ - name: "receives multiplexed input signal ", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - //r2 expects 10 to come from "gen", but actually it is getting modified by r1 caused undesired behaviour - outputs.byName("o1").setValue(inputs.byName("i1").getValue()) - return nil - }, - } - - //Define pipes - gen.outputs.byName("num").CreatePipesTo(r1.inputs.byName("i1"), r2.inputs.byName("i1")) - - //Build mesh - fm := FMesh{ - Components: Components{gen, r1, r2}, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - - gen.inputs.byName("i1").setValue(&SingleSignal{val: 10}) - - //Run the mesh - hops, err := fm.run() - _ = hops - - res := r2.outputs.byName("o1").getValue() - - fmt.Printf("r2 received : %v", res.(*SingleSignal).GetVal()) - - if err != nil { - fmt.Println(err) - } - -} diff --git a/experiments/10_shared_pointer/pipe.go b/experiments/10_shared_pointer/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/10_shared_pointer/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/10_shared_pointer/port.go b/experiments/10_shared_pointer/port.go deleted file mode 100644 index f056109..0000000 --- a/experiments/10_shared_pointer/port.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(*AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipesTo(toPorts ...*Port) { - for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } - -} diff --git a/experiments/10_shared_pointer/ports.go b/experiments/10_shared_pointer/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/10_shared_pointer/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/10_shared_pointer/signal.go b/experiments/10_shared_pointer/signal.go deleted file mode 100644 index c4caeb4..0000000 --- a/experiments/10_shared_pointer/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetVal() any { - return s.val -} diff --git a/experiments/11_nested_meshes/component.go b/experiments/11_nested_meshes/component.go deleted file mode 100644 index 563c713..0000000 --- a/experiments/11_nested_meshes/component.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -// @TODO: add a builder pattern implementation -type Component struct { - name string - description string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasSignal() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.flush() - } - out.clearSignal() - } -} diff --git a/experiments/11_nested_meshes/components.go b/experiments/11_nested_meshes/components.go deleted file mode 100644 index 95a556e..0000000 --- a/experiments/11_nested_meshes/components.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Components []*Component - -func (components Components) byName(name string) *Component { - for _, c := range components { - if c.name == name { - return c - } - } - return nil -} diff --git a/experiments/11_nested_meshes/errors.go b/experiments/11_nested_meshes/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/11_nested_meshes/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/11_nested_meshes/fmesh.go b/experiments/11_nested_meshes/fmesh.go deleted file mode 100644 index 9439f9a..0000000 --- a/experiments/11_nested_meshes/fmesh.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Name string - Description string - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/11_nested_meshes/go.mod b/experiments/11_nested_meshes/go.mod deleted file mode 100644 index f8658fe..0000000 --- a/experiments/11_nested_meshes/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/11_nested_meshes - -go 1.23 diff --git a/experiments/11_nested_meshes/main.go b/experiments/11_nested_meshes/main.go deleted file mode 100644 index 1fc89fd..0000000 --- a/experiments/11_nested_meshes/main.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "fmt" -) - -// This example demonstrates the ability of meshes to be nested (a component of mesh can be a mesh itself and nesting depth is unlimited) -func main() { - //Define components - c1 := &Component{ - name: "math", - description: "a * b + c", - inputs: Ports{ - "a": &Port{}, - "b": &Port{}, - "c": &Port{}, - }, - outputs: Ports{ - "out": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - if !inputs.manyByName("a", "b", "c").allHaveValue() { - return errWaitingForInputKeepInputs - } - - //This component is using a nested mesh for multiplication - multiplierWithLogger := getSubMesh() - - //Pass inputs - forwardSignal(inputs.byName("a"), multiplierWithLogger.Components.byName("Multiplier").inputs.byName("a")) - forwardSignal(inputs.byName("b"), multiplierWithLogger.Components.byName("Multiplier").inputs.byName("b")) - - //Run submesh inside a component - multiplierWithLogger.run() - - //Read the multiplication result - multiplicationResult := multiplierWithLogger.Components.byName("Multiplier").outputs.byName("result").getSignal().GetValue().(int) - - //Do the rest of calculation - res := multiplicationResult + inputs.byName("c").getSignal().GetValue().(int) - - outputs.byName("out").putSignal(newSignal(res)) - return nil - }, - } - - c2 := &Component{ - name: "add constant", - description: "a + 35", - inputs: Ports{ - "a": &Port{}, - }, - outputs: Ports{ - "out": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - if inputs.byName("a").hasSignal() { - a := inputs.byName("a").getSignal().GetValue().(int) - outputs.byName("out").putSignal(newSignal(a + 35)) - } - return nil - }, - } - - //Define pipes - c1.outputs.byName("out").CreatePipesTo(c2.inputs.byName("a")) - - //Build mesh - fm := &FMesh{ - Components: Components{c1, c2}, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - - c1.inputs.byName("a").putSignal(newSignal(2)) - c1.inputs.byName("b").putSignal(newSignal(3)) - c1.inputs.byName("c").putSignal(newSignal(4)) - - //Run the mesh - hops, err := fm.run() - if err != nil { - fmt.Println(err) - } - _ = hops - - res := c2.outputs.byName("out").getSignal().GetValue() - - fmt.Printf("outter fmesh result %v", res) -} - -func getSubMesh() *FMesh { - multiplier := &Component{ - name: "Multiplier", - description: "This component multiplies numbers on it's inputs", - inputs: Ports{ - "a": &Port{}, - "b": &Port{}, - }, - outputs: Ports{ - "bypass_a": &Port{}, - "bypass_b": &Port{}, - - "result": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - //@TODO: simplify waiting API - if !inputs.manyByName("a", "b").allHaveValue() { - return errWaitingForInputKeepInputs - } - - //Bypass input signals, so logger can get them - forwardSignal(inputs.byName("a"), outputs.byName("bypass_a")) - forwardSignal(inputs.byName("b"), outputs.byName("bypass_b")) - - a, b := inputs.byName("a").getSignal().GetValue().(int), inputs.byName("b").getSignal().GetValue().(int) - - outputs.byName("result").putSignal(newSignal(a * b)) - return nil - }, - } - - logger := &Component{ - name: "Logger", - description: "This component logs inputs of multiplier", - inputs: Ports{ - "a": &Port{}, - "b": &Port{}, - }, - outputs: nil, //No output - handler: func(inputs Ports, outputs Ports) error { - if inputs.byName("a").hasSignal() { - fmt.Println(fmt.Sprintf("Inner logger says: a is %v", inputs.byName("a").getSignal().GetValue())) - } - - if inputs.byName("b").hasSignal() { - fmt.Println(fmt.Sprintf("Inner logger says: b is %v", inputs.byName("b").getSignal().GetValue())) - } - - return nil - }, - } - - multiplier.outputs.byName("bypass_a").CreatePipesTo(logger.inputs.byName("a")) - multiplier.outputs.byName("bypass_b").CreatePipesTo(logger.inputs.byName("b")) - - return &FMesh{ - Name: "Logged multiplicator", - Description: "multiply 2 numbers and log inputs into std out", - Components: Components{multiplier, logger}, - ErrorHandlingStrategy: StopOnFirstError, - } -} diff --git a/experiments/11_nested_meshes/pipe.go b/experiments/11_nested_meshes/pipe.go deleted file mode 100644 index 8737a62..0000000 --- a/experiments/11_nested_meshes/pipe.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - forwardSignal(p.From, p.To) -} diff --git a/experiments/11_nested_meshes/port.go b/experiments/11_nested_meshes/port.go deleted file mode 100644 index 7350a2c..0000000 --- a/experiments/11_nested_meshes/port.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -type Port struct { - signal Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getSignal() Signal { - if p.signal.IsAggregate() { - return p.signal.(*AggregateSignal) - } - return p.signal.(*SingleSignal) -} - -func (p *Port) putSignal(sig Signal) { - if p.hasSignal() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.signal.IsSingle() { - resValues = append(resValues, p.signal.(*SingleSignal)) - } else if p.signal.IsAggregate() { - resValues = p.signal.(*AggregateSignal).val - } - - //Add new signal(s) - if sig.IsSingle() { - resValues = append(resValues, sig.(*SingleSignal)) - } else if sig.IsAggregate() { - resValues = append(resValues, sig.(*AggregateSignal).val...) - } - - p.signal = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.signal = sig -} - -func (p *Port) clearSignal() { - p.signal = nil -} - -func (p *Port) hasSignal() bool { - return p.signal != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipesTo(toPorts ...*Port) { - for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } - -} diff --git a/experiments/11_nested_meshes/ports.go b/experiments/11_nested_meshes/ports.go deleted file mode 100644 index b53e6d2..0000000 --- a/experiments/11_nested_meshes/ports.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -// @TODO: this type must have good tooling for working with collection -// like adding new ports, filtering and so on -type Ports map[string]*Port - -// @TODO: add error handling (e.g. when port does not exist) -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasSignal() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasSignal() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.putSignal(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearSignal() - } -} diff --git a/experiments/11_nested_meshes/signal.go b/experiments/11_nested_meshes/signal.go deleted file mode 100644 index 17d7a9c..0000000 --- a/experiments/11_nested_meshes/signal.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool - GetValue() any -} - -// @TODO: enhance naming -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) GetValue() any { - return s.val -} - -func (s SingleSignal) GetValue() any { - return s.val -} - -func newSignal(val any) *SingleSignal { - return &SingleSignal{val: val} -} - -func forwardSignal(source *Port, dest *Port) { - dest.putSignal(source.getSignal()) -} diff --git a/experiments/12_async_input/component.go b/experiments/12_async_input/component.go deleted file mode 100644 index 563c713..0000000 --- a/experiments/12_async_input/component.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -// @TODO: add a builder pattern implementation -type Component struct { - name string - description string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasSignal() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.flush() - } - out.clearSignal() - } -} diff --git a/experiments/12_async_input/components.go b/experiments/12_async_input/components.go deleted file mode 100644 index 95a556e..0000000 --- a/experiments/12_async_input/components.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Components []*Component - -func (components Components) byName(name string) *Component { - for _, c := range components { - if c.name == name { - return c - } - } - return nil -} diff --git a/experiments/12_async_input/errors.go b/experiments/12_async_input/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/12_async_input/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/12_async_input/fmesh.go b/experiments/12_async_input/fmesh.go deleted file mode 100644 index 9439f9a..0000000 --- a/experiments/12_async_input/fmesh.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Name string - Description string - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/12_async_input/go.mod b/experiments/12_async_input/go.mod deleted file mode 100644 index ba77271..0000000 --- a/experiments/12_async_input/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/12_async_input - -go 1.23 diff --git a/experiments/12_async_input/main.go b/experiments/12_async_input/main.go deleted file mode 100644 index ca108ec..0000000 --- a/experiments/12_async_input/main.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "time" -) - -// This example demonstrates how fmesh can be fed input asynchronously -func main() { - //exampleFeedBatches() - exampleFeedSequentially() -} - -// This example processes 1 url every 3 seconds -// NOTE: urls are not crawled concurrently, because fm has only 1 worker (crawler component) -func exampleFeedSequentially() { - fm := getMesh() - - urls := []string{ - "http://fffff.com", - "https://google.com", - "http://habr.com", - "http://localhost:80", - "https://postman-echo.com/delay/1", - "https://postman-echo.com/delay/3", - "https://postman-echo.com/delay/5", - "https://postman-echo.com/delay/10", - } - - ticker := time.NewTicker(3 * time.Second) - resultsChan := make(chan []any) - doneChan := make(chan struct{}) // Signals when all urls are processed - - //Feeder routine - go func() { - for { - select { - case <-ticker.C: - if len(urls) == 0 { - close(resultsChan) - return - } - //Pop an url - url := urls[0] - urls = urls[1:] - - fmt.Println("feed this url:", url) - - fm.Components.byName("web crawler").inputs.byName("url").putSignal(newSignal(url)) - _, err := fm.run() - if err != nil { - fmt.Println("fmesh returned error ", err) - } - - if fm.Components.byName("web crawler").outputs.byName("headers").hasSignal() { - results := fm.Components.byName("web crawler").outputs.byName("headers").getSignal().AllValues() - fm.Components.byName("web crawler").outputs.byName("headers").clearSignal() //@TODO maybe we can add fm.Reset() for cases when FMesh is reused (instead of cleaning ports explicitly) - resultsChan <- results - } - } - } - }() - - //Result reader routine - go func() { - for { - select { - case r, ok := <-resultsChan: - if !ok { - fmt.Println("results chan is closed. shutting down the reader") - doneChan <- struct{}{} - return - } - fmt.Println(fmt.Sprintf("got results from channel: %v", r)) - } - } - }() - - <-doneChan -} - -// This example leverages signal aggregation, so urls are pushed into fmesh all at once -// so we wait for all urls to be processed and only them we can read results -func exampleFeedBatches() { - batch := []string{ - "http://fffff.com", - "https://google.com", - "http://habr.com", - "http://localhost:80", - } - - fm := getMesh() - - for _, url := range batch { - fm.Components.byName("web crawler").inputs.byName("url").putSignal(newSignal(url)) - } - - _, err := fm.run() - if err != nil { - fmt.Println("fmesh returned error ", err) - } - - if fm.Components.byName("web crawler").outputs.byName("headers").hasSignal() { - results := fm.Components.byName("web crawler").outputs.byName("headers").getSignal().AllValues() - fmt.Printf("results: %v", results) - } -} - -func getMesh() *FMesh { - //Setup dependencies - client := &http.Client{} - - //Define components - crawler := &Component{ - name: "web crawler", - description: "gets http headers from given url", - inputs: Ports{ - "url": &Port{}, - }, - outputs: Ports{ - "errors": &Port{}, - "headers": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - if !inputs.byName("url").hasSignal() { - return errWaitingForInputResetInputs - } - - for _, sigVal := range inputs.byName("url").getSignal().AllValues() { - - url := sigVal.(string) - //All urls incoming as aggregatet signal will be crawled sequentially - // in order to call them concurrently we need run each request in separate goroutine and handle synchronization (e.g. waitgroup) - response, err := client.Get(url) - if err != nil { - outputs.byName("errors").putSignal(newSignal(fmt.Errorf("got error: %w from url: %s", err, url))) - continue - } - - if len(response.Header) == 0 { - outputs.byName("errors").putSignal(newSignal(fmt.Errorf("no headers for url %s", url))) - continue - } - - outputs.byName("headers").putSignal(newSignal(map[string]http.Header{ - url: response.Header, - })) - } - - return nil - }, - } - - logger := &Component{ - name: "error logger", - description: "logs http errors", - inputs: Ports{ - "error": &Port{}, - }, - outputs: nil, - handler: func(inputs Ports, outputs Ports) error { - if !inputs.byName("error").hasSignal() { - return errWaitingForInputResetInputs - } - - for _, sigVal := range inputs.byName("error").getSignal().AllValues() { - err := sigVal.(error) - if err != nil { - fmt.Println("Error logger says:", err) - } - } - - return nil - }, - } - - //Define pipes - crawler.outputs.byName("errors").CreatePipesTo(logger.inputs.byName("error")) - - //Build mesh - return &FMesh{ - Components: Components{crawler, logger}, - ErrorHandlingStrategy: StopOnFirstError, - } - -} diff --git a/experiments/12_async_input/pipe.go b/experiments/12_async_input/pipe.go deleted file mode 100644 index 8737a62..0000000 --- a/experiments/12_async_input/pipe.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - forwardSignal(p.From, p.To) -} diff --git a/experiments/12_async_input/port.go b/experiments/12_async_input/port.go deleted file mode 100644 index 97c1358..0000000 --- a/experiments/12_async_input/port.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -type Port struct { - signal Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getSignal() Signal { - if p == nil { - panic("invalid port") - } - - if !p.hasSignal() { - return nil - } - - if p.signal.IsAggregate() { - return p.signal.(*AggregateSignal) - } - return p.signal.(*SingleSignal) -} - -func (p *Port) putSignal(sig Signal) { - if p.hasSignal() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.signal.IsSingle() { - resValues = append(resValues, p.signal.(*SingleSignal)) - } else if p.signal.IsAggregate() { - resValues = p.signal.(*AggregateSignal).val - } - - //Add new signal(s) - if sig.IsSingle() { - resValues = append(resValues, sig.(*SingleSignal)) - } else if sig.IsAggregate() { - resValues = append(resValues, sig.(*AggregateSignal).val...) - } - - p.signal = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.signal = sig -} - -func (p *Port) clearSignal() { - p.signal = nil -} - -func (p *Port) hasSignal() bool { - return p.signal != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipesTo(toPorts ...*Port) { - for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } - -} diff --git a/experiments/12_async_input/ports.go b/experiments/12_async_input/ports.go deleted file mode 100644 index b53e6d2..0000000 --- a/experiments/12_async_input/ports.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -// @TODO: this type must have good tooling for working with collection -// like adding new ports, filtering and so on -type Ports map[string]*Port - -// @TODO: add error handling (e.g. when port does not exist) -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasSignal() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasSignal() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.putSignal(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearSignal() - } -} diff --git a/experiments/12_async_input/signal.go b/experiments/12_async_input/signal.go deleted file mode 100644 index 55346b9..0000000 --- a/experiments/12_async_input/signal.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool - GetValue() any - AllValues() []any //@TODO: refactor with true iterator -} - -// @TODO: enhance naming -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) GetValue() any { - return s.val -} - -func (s SingleSignal) GetValue() any { - return s.val -} - -func (s SingleSignal) AllValues() []any { - return []any{s.val} -} - -func (s AggregateSignal) AllValues() []any { - all := make([]any, 0) - for _, sig := range s.val { - all = append(all, sig.GetValue()) - } - return all -} - -func newSignal(val any) *SingleSignal { - return &SingleSignal{val: val} -} - -func forwardSignal(source *Port, dest *Port) { - dest.putSignal(source.getSignal()) -} diff --git a/experiments/1_function_composition/common.go b/experiments/1_function_composition/common.go deleted file mode 100644 index 93ac32e..0000000 --- a/experiments/1_function_composition/common.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -func Add(a int, b int) int { - return a + b -} - -func Mul(a int, b int) int { - return a * b -} diff --git a/experiments/1_function_composition/fmesh.go b/experiments/1_function_composition/fmesh.go deleted file mode 100644 index 4bab26b..0000000 --- a/experiments/1_function_composition/fmesh.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import "fmt" - -type FMesh struct { - Components []*Component - Pipes []*Pipe -} - -func (fm *FMesh) compute() { - for _, c := range fm.Components { - c.compute() - } -} - -func (fm *FMesh) flush() { - for _, p := range fm.Pipes { - p.flush() - } -} - -func (fm *FMesh) hasComponentToCompute() bool { - for _, c := range fm.Components { - if c.hasInput() { - return true - } - } - return false -} - -func RunAsFMesh(input int) { - c1 := Component{ - Name: "mul 2", - } - c1.h = func(input int) int { - return Mul(input, 2) - } - c1.setInput(10) - - c2 := Component{ - Name: "add 3", - } - c2.h = func(input int) int { - return Add(input, 3) - } - - c3 := Component{ - Name: "add 5", - } - c3.h = func(input int) int { - return Add(input, 5) - } - - fmesh := &FMesh{ - Components: []*Component{&c1, &c2, &c3}, - Pipes: []*Pipe{ - &Pipe{ - In: &c1, - Out: &c2, - }, - &Pipe{ - In: &c2, - Out: &c3, - }, - }, - } - - for fmesh.hasComponentToCompute() { - fmesh.compute() - fmesh.flush() - } - - res := c3.getOutput() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/1_function_composition/fmesh_component.go b/experiments/1_function_composition/fmesh_component.go deleted file mode 100644 index b01a673..0000000 --- a/experiments/1_function_composition/fmesh_component.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -type Component struct { - Name string - input int - h func(input int) int - output int -} - -func (c *Component) getOutput() int { - return c.output -} - -func (c *Component) hasInput() bool { - return c.input != 0 -} - -func (c *Component) setInput(input int) { - c.input = input -} - -func (c *Component) setOutput(output int) { - c.output = output -} - -func (c *Component) clearInput() { - c.input = 0 -} - -func (c *Component) clearOutput() { - c.output = 0 -} - -func (c *Component) compute() { - if !c.hasInput() { - return - } - c.output = c.h(c.input) - c.clearInput() -} diff --git a/experiments/1_function_composition/fmesh_pipe.go b/experiments/1_function_composition/fmesh_pipe.go deleted file mode 100644 index 1e9e11f..0000000 --- a/experiments/1_function_composition/fmesh_pipe.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -type Pipe struct { - In *Component - Out *Component -} - -func (p *Pipe) flush() { - p.Out.setInput(p.In.getOutput()) - p.In.clearOutput() -} diff --git a/experiments/1_function_composition/go.mod b/experiments/1_function_composition/go.mod deleted file mode 100644 index 64395aa..0000000 --- a/experiments/1_function_composition/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/1_function_composition - -go 1.23 diff --git a/experiments/1_function_composition/main.go b/experiments/1_function_composition/main.go deleted file mode 100644 index 1056afc..0000000 --- a/experiments/1_function_composition/main.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -// This demo shows different ways to compose 3 arithmetic operations: 10*2+3+5 -func main() { - //RunAsNestedFuncs(10) - //RunAsPipes(10) - RunAsFMesh(10) -} diff --git a/experiments/1_function_composition/nested_funcs.go b/experiments/1_function_composition/nested_funcs.go deleted file mode 100644 index d24332d..0000000 --- a/experiments/1_function_composition/nested_funcs.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import "fmt" - -func RunAsNestedFuncs(input int) { - res := Add(Add(Mul(input, 2), 3), 5) - - fmt.Printf("Result is %v", res) -} diff --git a/experiments/1_function_composition/pipes.go b/experiments/1_function_composition/pipes.go deleted file mode 100644 index d0fdb9e..0000000 --- a/experiments/1_function_composition/pipes.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" -) - -func RunAsPipes(input int) { - ch1 := make(chan int) - ch2 := make(chan int) - done := make(chan struct{}) - var res int - - go func() { - ch1 <- Mul(input, 2) - }() - - go func() { - ch2 <- Add(<-ch1, 3) - }() - - go func() { - res = Add(<-ch2, 5) - done <- struct{}{} - - }() - - <-done - fmt.Printf("Result is %v", res) -} diff --git a/experiments/2_multiple_inputs_outputs/component.go b/experiments/2_multiple_inputs_outputs/component.go deleted file mode 100644 index 67c6e51..0000000 --- a/experiments/2_multiple_inputs_outputs/component.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) -} - -type Components []*Component - -func (c *Component) compute() { - if !c.inputs.anyHasValue() { - //No inputs set, nothing to compute - return - } - //Run the computation - c.handler(c.inputs, c.outputs) - - //Clear inputs - c.inputs.setAll(nil) -} diff --git a/experiments/2_multiple_inputs_outputs/fmesh.go b/experiments/2_multiple_inputs_outputs/fmesh.go deleted file mode 100644 index 2210db0..0000000 --- a/experiments/2_multiple_inputs_outputs/fmesh.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type FMesh struct { - Components Components - Pipes Pipes -} - -func (fm *FMesh) compute() { - for _, c := range fm.Components { - c.compute() - } -} - -func (fm *FMesh) flush() { - for _, p := range fm.Pipes { - p.flush() - } -} - -func (fm *FMesh) run() { - for fm.hasComponentToCompute() { - fm.compute() - fm.flush() - } -} - -func (fm *FMesh) hasComponentToCompute() bool { - for _, c := range fm.Components { - if c.inputs.anyHasValue() { - return true - } - } - return false -} diff --git a/experiments/2_multiple_inputs_outputs/go.mod b/experiments/2_multiple_inputs_outputs/go.mod deleted file mode 100644 index 7b88dce..0000000 --- a/experiments/2_multiple_inputs_outputs/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/2_multiple_inputs_outputs - -go 1.23 diff --git a/experiments/2_multiple_inputs_outputs/main.go b/experiments/2_multiple_inputs_outputs/main.go deleted file mode 100644 index d889646..0000000 --- a/experiments/2_multiple_inputs_outputs/main.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import "fmt" - -// This experiment aims to demonstrate components with multiple inputs and outputs -// In this case the component acts like a multivariable function -func main() { - - //Define components - c1 := &Component{ - name: "mul 2, mul 10", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - "o2": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) { - i1, i2 := inputs.byName("i1"), inputs.byName("i2") - if i1.hasValue() && i2.hasValue() { - //Merge 2 input signals and put it onto single output - c := *i1.getValue()*2 + *i2.getValue()*10 - outputs.byName("o1").setValue(&c) - } - - //We can generate output signal without any input - c4 := 4 - outputs.byName("o2").setValue(&c4) - }, - } - - c2 := &Component{ - name: "add 3 or input", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) { - c := 3 - if inputs.byName("i2").hasValue() { - c = *inputs.byName("i2").getValue() - } - t := *inputs.byName("i1").getValue() + c - outputs.byName("o1").setValue(&t) - }, - } - - c3 := &Component{ - name: "add 5", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) { - t := *inputs.byName("i1").getValue() + 5 - outputs.byName("o1").setValue(&t) - }, - } - - //Define pipes - pipes := Pipes{ - &Pipe{ - In: c1.outputs.byName("o1"), - Out: c2.inputs.byName("i1"), - }, - &Pipe{ - In: c1.outputs.byName("o2"), - Out: c2.inputs.byName("i2"), - }, - &Pipe{ - In: c2.outputs.byName("o1"), - Out: c3.inputs.byName("i1"), - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, c3, - }, - Pipes: pipes, - } - - //Set inputs - a, b := 10, 20 - c1.inputs.byName("i1").setValue(&a) - c1.inputs.byName("i2").setValue(&b) - - //Run the mesh - fm.run() - - //Read outputs - res := *c3.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/2_multiple_inputs_outputs/pipe.go b/experiments/2_multiple_inputs_outputs/pipe.go deleted file mode 100644 index 3b9ca7e..0000000 --- a/experiments/2_multiple_inputs_outputs/pipe.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -type Pipe struct { - In *Port - Out *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - p.Out.setValue(p.In.getValue()) - p.In.setValue(nil) -} diff --git a/experiments/2_multiple_inputs_outputs/port.go b/experiments/2_multiple_inputs_outputs/port.go deleted file mode 100644 index 7a2a2e2..0000000 --- a/experiments/2_multiple_inputs_outputs/port.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -type PortValue *int - -type Port struct { - val PortValue -} - -func (p *Port) getValue() PortValue { - return p.val -} - -func (p *Port) setValue(val PortValue) { - p.val = val -} - -func (p *Port) hasValue() bool { - return p.val != nil -} diff --git a/experiments/2_multiple_inputs_outputs/ports.go b/experiments/2_multiple_inputs_outputs/ports.go deleted file mode 100644 index 745f295..0000000 --- a/experiments/2_multiple_inputs_outputs/ports.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val PortValue) { - for _, p := range ports { - p.setValue(val) - } -} diff --git a/experiments/3_concurrent_computing/component.go b/experiments/3_concurrent_computing/component.go deleted file mode 100644 index d84e0a1..0000000 --- a/experiments/3_concurrent_computing/component.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "errors" - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) compute() error { - if !c.inputs.anyHasValue() { - //No inputs set, nothing to compute - return errors.New("no inputs set") - } - //Run the computation - c.handler(c.inputs, c.outputs) - - //Clear inputs - c.inputs.setAll(nil) - return nil -} diff --git a/experiments/3_concurrent_computing/fmesh.go b/experiments/3_concurrent_computing/fmesh.go deleted file mode 100644 index ba04dbf..0000000 --- a/experiments/3_concurrent_computing/fmesh.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "sync" - "sync/atomic" -) - -type FMesh struct { - Components Components - Pipes Pipes -} - -func (fm *FMesh) compute() int64 { - var wg sync.WaitGroup - var componentsTriggered int64 - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - err := c.compute() - - if err == nil { - atomic.AddInt64(&componentsTriggered, 1) - } - }() - } - - wg.Wait() - return componentsTriggered -} - -func (fm *FMesh) flush() { - for _, p := range fm.Pipes { - p.flush() - } -} - -func (fm *FMesh) run() { - for { - componentsTriggered := fm.compute() - - if componentsTriggered == 0 { - return - } - fm.flush() - } -} diff --git a/experiments/3_concurrent_computing/go.mod b/experiments/3_concurrent_computing/go.mod deleted file mode 100644 index 1f98efb..0000000 --- a/experiments/3_concurrent_computing/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/3_concurrent_computing - -go 1.23 diff --git a/experiments/3_concurrent_computing/main.go b/experiments/3_concurrent_computing/main.go deleted file mode 100644 index 2e76abf..0000000 --- a/experiments/3_concurrent_computing/main.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment demonstrates ability to run computations of N components (which are in the same "hop") concurrently -func main() { - //Define components - m1 := &Component{ - name: "m1", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 * 2 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - m2 := &Component{ - name: "m2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 * 3 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - a1 := &Component{ - name: "a1", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 + 50 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - a2 := &Component{ - name: "a2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 + 100 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - comb := &Component{ - name: "combiner", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1, v2 := *inputs.byName("i1").getValue(), *inputs.byName("i2").getValue() - r := v1 + v2 - outputs.byName("o1").setValue(&r) - return nil - }, - } - - //Define pipes - pipes := Pipes{ - &Pipe{ - From: m1.outputs.byName("o1"), - To: a1.inputs.byName("i1"), - }, - &Pipe{ - From: m2.outputs.byName("o1"), - To: a2.inputs.byName("i1"), - }, - &Pipe{ - From: a1.outputs.byName("o1"), - To: comb.inputs.byName("i1"), - }, - &Pipe{ - From: a2.outputs.byName("o1"), - To: comb.inputs.byName("i2"), - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - m1, m2, a1, a2, comb, - }, - Pipes: pipes, - } - - //Set inputs - a, b := 10, 20 - m1.inputs.byName("i1").setValue(&a) - m2.inputs.byName("i1").setValue(&b) - - //Run the mesh - fm.run() - - //Read outputs - res := *comb.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/3_concurrent_computing/pipe.go b/experiments/3_concurrent_computing/pipe.go deleted file mode 100644 index 68451e1..0000000 --- a/experiments/3_concurrent_computing/pipe.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - p.To.setValue(p.From.getValue()) - p.From.setValue(nil) -} diff --git a/experiments/3_concurrent_computing/port.go b/experiments/3_concurrent_computing/port.go deleted file mode 100644 index 7a2a2e2..0000000 --- a/experiments/3_concurrent_computing/port.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -type PortValue *int - -type Port struct { - val PortValue -} - -func (p *Port) getValue() PortValue { - return p.val -} - -func (p *Port) setValue(val PortValue) { - p.val = val -} - -func (p *Port) hasValue() bool { - return p.val != nil -} diff --git a/experiments/3_concurrent_computing/ports.go b/experiments/3_concurrent_computing/ports.go deleted file mode 100644 index 745f295..0000000 --- a/experiments/3_concurrent_computing/ports.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val PortValue) { - for _, p := range ports { - p.setValue(val) - } -} diff --git a/experiments/4_multiplexed_pipes/component.go b/experiments/4_multiplexed_pipes/component.go deleted file mode 100644 index 656f9bb..0000000 --- a/experiments/4_multiplexed_pipes/component.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import "errors" - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() error { - if !c.inputs.anyHasValue() { - //No inputs set, nothing to activateComponents - return errors.New("no inputs set") - } - //Run the computation - err := c.handler(c.inputs, c.outputs) - if err != nil { - return err - } - - //Clear inputs - c.inputs.clearAll() - return nil -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/4_multiplexed_pipes/fmesh.go b/experiments/4_multiplexed_pipes/fmesh.go deleted file mode 100644 index bbfd32f..0000000 --- a/experiments/4_multiplexed_pipes/fmesh.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "sync" - "sync/atomic" -) - -type FMesh struct { - Components Components -} - -func (fm *FMesh) activateComponents() int64 { - var wg sync.WaitGroup - var componentsTriggered int64 - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - err := c.activate() - - if err == nil { - atomic.AddInt64(&componentsTriggered, 1) - } - }() - } - - wg.Wait() - return componentsTriggered -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() { - for { - componentsTriggered := fm.activateComponents() - - if componentsTriggered == 0 { - return - } - fm.flushPipes() - } -} diff --git a/experiments/4_multiplexed_pipes/go.mod b/experiments/4_multiplexed_pipes/go.mod deleted file mode 100644 index e7371e4..0000000 --- a/experiments/4_multiplexed_pipes/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/4_multiplexed_pipes - -go 1.23 diff --git a/experiments/4_multiplexed_pipes/main.go b/experiments/4_multiplexed_pipes/main.go deleted file mode 100644 index 985557f..0000000 --- a/experiments/4_multiplexed_pipes/main.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment add pipes multiplexing and signal aggregation -// - if N pipes are reading from the same port they will receive the same signal in next hop -// - if N pipes are writing to the same port in given hop the resulting signal will be an aggregation of all signals from all pipes -func main() { - //Define components - c1 := &Component{ - name: "add 3", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 3, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c2 := &Component{ - name: "mul 2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 * 2, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c3 := &Component{ - name: "add 5", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 5, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c4 := &Component{ - name: "agg sum", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsAggregate() { - a1 := (i1).(*AggregateSignal) - - sum := 0 - for _, v := range a1.val { - sum += v.GetInt() - } - - resSignal = &SingleSignal{ - val: sum, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, c3, c4, - }, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c1.outputs.byName("o1").CreatePipeTo(c3.inputs.byName("i1")) - c2.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - c3.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - - //Set inputs - a := &SingleSignal{ - val: 10, - } - c1.inputs.byName("i1").setValue(a) - - //Run the mesh - fm.run() - - //Read outputs - res := c4.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/4_multiplexed_pipes/pipe.go b/experiments/4_multiplexed_pipes/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/4_multiplexed_pipes/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/4_multiplexed_pipes/port.go b/experiments/4_multiplexed_pipes/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/4_multiplexed_pipes/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/4_multiplexed_pipes/ports.go b/experiments/4_multiplexed_pipes/ports.go deleted file mode 100644 index 0e4ea4f..0000000 --- a/experiments/4_multiplexed_pipes/ports.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/4_multiplexed_pipes/signal.go b/experiments/4_multiplexed_pipes/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/4_multiplexed_pipes/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/5_error_handling/component.go b/experiments/5_error_handling/component.go deleted file mode 100644 index 93bddc9..0000000 --- a/experiments/5_error_handling/component.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() ActivationResult { - if !c.inputs.anyHasValue() { - //No inputs set, stop here - return ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - } - //Run the computation - err := c.handler(c.inputs, c.outputs) - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - return ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - } - - return ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/5_error_handling/errors.go b/experiments/5_error_handling/errors.go deleted file mode 100644 index 2805f8e..0000000 --- a/experiments/5_error_handling/errors.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import "sync" - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} diff --git a/experiments/5_error_handling/fmesh.go b/experiments/5_error_handling/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/5_error_handling/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/5_error_handling/go.mod b/experiments/5_error_handling/go.mod deleted file mode 100644 index 4564f44..0000000 --- a/experiments/5_error_handling/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/5_error_handling - -go 1.23 diff --git a/experiments/5_error_handling/main.go b/experiments/5_error_handling/main.go deleted file mode 100644 index c1f3706..0000000 --- a/experiments/5_error_handling/main.go +++ /dev/null @@ -1,161 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -// This experiment enhances error handling capabilities -func main() { - //Define components - - c0 := &Component{ - name: "this component just brakes on start", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - return errors.New("i'm feelin baaad") - }, - } - - c1 := &Component{ - name: "add 3", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 3, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c2 := &Component{ - name: "mul 2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 * 2, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c3 := &Component{ - name: "add 5", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 5, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c4 := &Component{ - name: "agg sum", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsAggregate() { - a1 := (i1).(*AggregateSignal) - - sum := 0 - for _, v := range a1.val { - sum += v.GetInt() - } - - resSignal = &SingleSignal{ - val: sum, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Build mesh - fm := FMesh{ - ErrorHandlingStrategy: StopOnFirstError, - Components: Components{ - c0, c1, c2, c3, c4, - }, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c1.outputs.byName("o1").CreatePipeTo(c3.inputs.byName("i1")) - c2.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - c3.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - - //Set inputs - a := &SingleSignal{ - val: 10, - } - c1.inputs.byName("i1").setValue(a) - c0.inputs.byName("i1").setValue(&SingleSignal{ - val: 34, - }) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - - //Read outputs - res := c4.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/5_error_handling/pipe.go b/experiments/5_error_handling/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/5_error_handling/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/5_error_handling/port.go b/experiments/5_error_handling/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/5_error_handling/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/5_error_handling/ports.go b/experiments/5_error_handling/ports.go deleted file mode 100644 index 0e4ea4f..0000000 --- a/experiments/5_error_handling/ports.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/5_error_handling/signal.go b/experiments/5_error_handling/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/5_error_handling/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/6_waiting_for_input/component.go b/experiments/6_waiting_for_input/component.go deleted file mode 100644 index ee47c37..0000000 --- a/experiments/6_waiting_for_input/component.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: errors.New("component panicked"), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/6_waiting_for_input/errors.go b/experiments/6_waiting_for_input/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/6_waiting_for_input/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/6_waiting_for_input/fmesh.go b/experiments/6_waiting_for_input/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/6_waiting_for_input/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/6_waiting_for_input/go.mod b/experiments/6_waiting_for_input/go.mod deleted file mode 100644 index 0dba9cc..0000000 --- a/experiments/6_waiting_for_input/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/6_waiting_for_input - -go 1.23 diff --git a/experiments/6_waiting_for_input/main.go b/experiments/6_waiting_for_input/main.go deleted file mode 100644 index b85e1ea..0000000 --- a/experiments/6_waiting_for_input/main.go +++ /dev/null @@ -1,176 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment shows how components can "wait" for particular inputs. -// Here we have c5 component which just sums up the outcomes of c4 and the chain of c1->c2->c3 -// As c4 output is ready in second hop the c5 is triggered, but the problem is the other part coming from the chain is not ready yet, -// because in second hop only c2 is triggered. So c5 has to wait somehow while both inputs will be ready. -// By sending special sentinel errors we instruct mesh how to treat given component. -// So starting from this version component can tell the mesh if it's inputs must be reset or kept. -func main() { - //Define components - c1 := &Component{ - name: "c1", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 3, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c2 := &Component{ - name: "c2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 5, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c3 := &Component{ - name: "c3", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 * 2, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c4 := &Component{ - name: "c4", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 10, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c5 := &Component{ - name: "c5", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - - // This component is a basic summator, it must trigger only when both inputs are set - if !inputs.manyByName("i1", "i2").allHaveValue() { - return errWaitingForInputKeepInputs - } - - i1 := inputs.byName("i1").getValue() - i2 := inputs.byName("i2").getValue() - - resSignal := &SingleSignal{ - val: (i1).(*SingleSignal).GetInt() + (i2).(*SingleSignal).GetInt(), - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, c3, c4, c5, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c2.outputs.byName("o1").CreatePipeTo(c3.inputs.byName("i1")) - c3.outputs.byName("o1").CreatePipeTo(c5.inputs.byName("i1")) - c4.outputs.byName("o1").CreatePipeTo(c5.inputs.byName("i2")) - - //Set inputs - a := &SingleSignal{ - val: 10, - } - - b := &SingleSignal{ - val: 2, - } - - c1.inputs.byName("i1").setValue(a) - c4.inputs.byName("i1").setValue(b) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - - //Read outputs - res := c5.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/6_waiting_for_input/pipe.go b/experiments/6_waiting_for_input/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/6_waiting_for_input/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/6_waiting_for_input/port.go b/experiments/6_waiting_for_input/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/6_waiting_for_input/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/6_waiting_for_input/ports.go b/experiments/6_waiting_for_input/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/6_waiting_for_input/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/6_waiting_for_input/signal.go b/experiments/6_waiting_for_input/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/6_waiting_for_input/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/7_loop/component.go b/experiments/7_loop/component.go deleted file mode 100644 index ee47c37..0000000 --- a/experiments/7_loop/component.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: errors.New("component panicked"), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/7_loop/errors.go b/experiments/7_loop/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/7_loop/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/7_loop/fmesh.go b/experiments/7_loop/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/7_loop/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/7_loop/go.mod b/experiments/7_loop/go.mod deleted file mode 100644 index 6d114d1..0000000 --- a/experiments/7_loop/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/7_loop - -go 1.23 diff --git a/experiments/7_loop/main.go b/experiments/7_loop/main.go deleted file mode 100644 index f6109e0..0000000 --- a/experiments/7_loop/main.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "fmt" - "strconv" -) - -// This experiment shows how a component can have a pipe looped in to it's input. -// This pattern allows to activate components multiple time using control plane (special output with looped in pipe) -func main() { - //Define components - c1 := &Component{ - name: "c1", - inputs: Ports{ - "i1": &Port{}, //Data plane - "i2": &Port{}, //Control plane - }, - outputs: Ports{ - "o1": &Port{}, //Data plane - "o2": &Port{}, //Control plane (loop) - }, - handler: func(inputs Ports, outputs Ports) error { - i1 := inputs.byName("i1").getValue() - - v1 := (i1).(*SingleSignal).GetInt() - - if v1 > 100 { - //Signal is ready to go to next component, breaking the loop - outputs.byName("o1").setValue(&SingleSignal{ - val: v1, - }) - } else { - //Loop in - outputs.byName("o2").setValue(&SingleSignal{ - val: v1 + 5, - }) - } - - return nil - }, - } - - c2 := &Component{ - name: "c2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - //Bypass i1->o1 - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: strconv.Itoa(v1) + " suffix added once", - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c1.outputs.byName("o2").CreatePipeTo(c1.inputs.byName("i1")) //Loop - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - a := &SingleSignal{ - val: 10, - } - - c1.inputs.byName("i1").setValue(a) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - - //Read outputs - res := c2.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/7_loop/pipe.go b/experiments/7_loop/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/7_loop/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/7_loop/port.go b/experiments/7_loop/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/7_loop/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/7_loop/ports.go b/experiments/7_loop/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/7_loop/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/7_loop/signal.go b/experiments/7_loop/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/7_loop/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/8_fibonacci/component.go b/experiments/8_fibonacci/component.go deleted file mode 100644 index ee47c37..0000000 --- a/experiments/8_fibonacci/component.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: errors.New("component panicked"), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/8_fibonacci/errors.go b/experiments/8_fibonacci/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/8_fibonacci/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/8_fibonacci/fmesh.go b/experiments/8_fibonacci/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/8_fibonacci/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/8_fibonacci/go.mod b/experiments/8_fibonacci/go.mod deleted file mode 100644 index 1ace83f..0000000 --- a/experiments/8_fibonacci/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/8_fibonacci - -go 1.23 diff --git a/experiments/8_fibonacci/main.go b/experiments/8_fibonacci/main.go deleted file mode 100644 index ed89a30..0000000 --- a/experiments/8_fibonacci/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment shows how a component can have a pipe looped in to it's input. -// This pattern allows to activate components multiple time using control plane (special output with looped in pipe) -func main() { - //Define components - c1 := &Component{ - name: "c1", - inputs: Ports{ - "i_cur": &Port{}, - "i_prev": &Port{}, - }, - outputs: Ports{ - "o_cur": &Port{}, - "o_prev": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - iCur := inputs.byName("i_cur").getValue() - iPrev := inputs.byName("i_prev").getValue() - - vCur := iCur.(*SingleSignal).GetInt() - vPrev := iPrev.(*SingleSignal).GetInt() - - vNext := vCur + vPrev - - if vNext < 150 { - fmt.Println(vNext) - outputs.byName("o_cur").setValue(&SingleSignal{val: vNext}) - outputs.byName("o_prev").setValue(&SingleSignal{val: vCur}) - } - - return nil - }, - } - - //Define pipes - c1.outputs.byName("o_cur").CreatePipeTo(c1.inputs.byName("i_cur")) - c1.outputs.byName("o_prev").CreatePipeTo(c1.inputs.byName("i_prev")) - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - f0 := &SingleSignal{ - val: 0, - } - - f1 := &SingleSignal{ - val: 1, - } - - c1.inputs.byName("i_prev").setValue(f0) - c1.inputs.byName("i_cur").setValue(f1) - - fmt.Println(f0.val) - fmt.Println(f1.val) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - -} diff --git a/experiments/8_fibonacci/pipe.go b/experiments/8_fibonacci/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/8_fibonacci/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/8_fibonacci/port.go b/experiments/8_fibonacci/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/8_fibonacci/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/8_fibonacci/ports.go b/experiments/8_fibonacci/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/8_fibonacci/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/8_fibonacci/signal.go b/experiments/8_fibonacci/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/8_fibonacci/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/9_complex_signal/component.go b/experiments/9_complex_signal/component.go deleted file mode 100644 index 772edc3..0000000 --- a/experiments/9_complex_signal/component.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/9_complex_signal/errors.go b/experiments/9_complex_signal/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/9_complex_signal/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/9_complex_signal/fmesh.go b/experiments/9_complex_signal/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/9_complex_signal/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/9_complex_signal/go.mod b/experiments/9_complex_signal/go.mod deleted file mode 100644 index c38204b..0000000 --- a/experiments/9_complex_signal/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/9_complex_signal - -go 1.23 diff --git a/experiments/9_complex_signal/main.go b/experiments/9_complex_signal/main.go deleted file mode 100644 index 91d2964..0000000 --- a/experiments/9_complex_signal/main.go +++ /dev/null @@ -1,376 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "strings" -) - -type Request struct { - Method string - Headers map[string]string - Body []byte -} - -type Response struct { - Status string - StatusCode int - Headers map[string]string - Body []byte -} - -// This experiment shows how signal can carry arbitrary data (not only primitive types) -// We use a simple round-robin load balancer as an example -func main() { - //Define components - lb := &Component{ - name: "Load Balancer", - inputs: Ports{ - "req:localhost:80": &Port{}, //Incoming requests - - //Responses from each backend: - "resp:backend1:80": &Port{}, - "resp:backend2:80": &Port{}, - "resp:backend3:80": &Port{}, - }, - outputs: Ports{ - //Requests to each backend - "req:backend1:80": &Port{}, - "req:backend2:80": &Port{}, - "req:backend3:80": &Port{}, - - "resp:localhost:80": &Port{}, //Out coming responses - }, - handler: func(inputs Ports, outputs Ports) error { - //Process incoming requests - reqInput := inputs.byName("req:localhost:80") - if reqInput.hasValue() { - reqSignals := reqInput.getValue() - - if reqSignals.IsAggregate() { - for _, sig := range reqSignals.(*AggregateSignal).val { - - //Handle authorization - req := sig.val.(*Request) - authHeader, ok := req.Headers["Auth"] - - if !ok || len(authHeader) == 0 { - //Missing auth header - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("response from LB: request: %s is missing auth header", string(req.Body))), - }}) - continue - } - - if authHeader != "Bearer 123" { - //Auth error - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("response from LB: request: %s is unauthorized", string(req.Body))), - }}) - continue - } - - targetBackendIndex := 1 + rand.Intn(3) - outputs.byName(fmt.Sprintf("req:backend%d:80", targetBackendIndex)).setValue(sig) - - } - - } else { - doDispatch := true - - sig := reqSignals.(*SingleSignal) - - //Handle authorization - req := sig.val.(*Request) - authHeader, ok := req.Headers["Auth"] - - if !ok || len(authHeader) == 0 { - //Missing auth header - doDispatch = false - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("Request: %s is missing auth header", string(req.Body))), - }}) - } - - if authHeader != "Bearer 123" { - //Auth error - doDispatch = false - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("Request: %s is unauthorized", string(req.Body))), - }}) - } - - if doDispatch { - targetBackendIndex := 1 + rand.Intn(3) - outputs.byName(fmt.Sprintf("req:backend%d:80", targetBackendIndex)).setValue(sig) - } - } - } - - //Read responses from backends and put them on main output for upstream consumer - for pname, port := range inputs { - if strings.Contains(pname, "resp:backend") && port.hasValue() { - outputs.byName("resp:localhost:80").setValue(port.getValue()) - } - } - - return nil - }, - } - - b1 := &Component{ - name: "Backend 1", - inputs: Ports{ - "req:localhost:80": &Port{}, - }, - outputs: Ports{ - "resp:localhost:80": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - reqInput := inputs.byName("req:localhost:80") - - if !reqInput.hasValue() { - return nil - } - - reqSignal := reqInput.getValue() - - if reqSignal.IsSingle() { - req := reqSignal.(*SingleSignal).GetVal().(*Request) - - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b1 for req:" + string(req.Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } else { - for _, sig := range reqSignal.(*AggregateSignal).val { - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b1 for req:" + string(sig.val.(*Request).Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } - } - return nil - }, - } - - b2 := &Component{ - name: "Backend 2", - inputs: Ports{ - "req:localhost:80": &Port{}, - }, - outputs: Ports{ - "resp:localhost:80": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - reqInput := inputs.byName("req:localhost:80") - - if !reqInput.hasValue() { - return nil - } - - reqSignal := reqInput.getValue() - - if reqSignal.IsSingle() { - req := reqSignal.(*SingleSignal).GetVal().(*Request) - - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b2 for req:" + string(req.Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } else { - for _, sig := range reqSignal.(*AggregateSignal).val { - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b2 for req:" + string(sig.val.(*Request).Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } - } - return nil - }, - } - - b3 := &Component{ - name: "Backend 3", - inputs: Ports{ - "req:localhost:80": &Port{}, - }, - outputs: Ports{ - "resp:localhost:80": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - reqInput := inputs.byName("req:localhost:80") - - if !reqInput.hasValue() { - return nil - } - - reqSignal := reqInput.getValue() - - if reqSignal.IsSingle() { - req := reqSignal.(*SingleSignal).GetVal().(*Request) - - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b3 for req:" + string(req.Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } else { - for _, sig := range reqSignal.(*AggregateSignal).val { - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b3 for req:" + string(sig.val.(*Request).Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } - } - return nil - }, - } - - //Define pipes - - //Request path - lb.outputs.byName("req:backend1:80").CreatePipeTo(b1.inputs.byName("req:localhost:80")) - lb.outputs.byName("req:backend2:80").CreatePipeTo(b2.inputs.byName("req:localhost:80")) - lb.outputs.byName("req:backend3:80").CreatePipeTo(b3.inputs.byName("req:localhost:80")) - - //Response path - b1.outputs.byName("resp:localhost:80").CreatePipeTo(lb.inputs.byName("resp:backend1:80")) - b2.outputs.byName("resp:localhost:80").CreatePipeTo(lb.inputs.byName("resp:backend2:80")) - b3.outputs.byName("resp:localhost:80").CreatePipeTo(lb.inputs.byName("resp:backend3:80")) - - //Build mesh - fm := FMesh{ - Components: Components{ - lb, b1, b2, b3, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - reqs := []*Request{ - { - Method: "GET", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 1"), - }, - { - Method: "POST", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 2"), - }, - { - Method: "PATCH", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 3"), - }, - - { - Method: "GET", - Headers: map[string]string{ - "Auth": "Bearer 777", //Gonna fail auth on LB: - }, - Body: []byte("request 4"), - }, - { - Method: "GET", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 5"), - }, - { - Method: "GET", - Headers: map[string]string{ - "Auth": "", //Empty auth header - }, - Body: []byte("request 6"), - }, - { - Method: "GET", - Headers: map[string]string{ - "Content-Type": "application/json", //Missing auth - }, - Body: []byte("request 7"), - }, - } - - //No need to create aggregated signal literal, - //we can perfectly set the value to same input, and it will be aggregated automatically - for _, req := range reqs { - lb.inputs.byName("req:localhost:80").setValue(&SingleSignal{val: req}) - //break //set just 1 request - } - - //Run the mesh - hops, err := fm.run() - _ = hops - - res := lb.outputs.byName("resp:localhost:80").getValue() - - if res.IsAggregate() { - for _, sig := range res.(*AggregateSignal).val { - resp := sig.val.(*Response) - - fmt.Println(string(resp.Body)) - } - } - - if err != nil { - fmt.Println(err) - } - -} diff --git a/experiments/9_complex_signal/pipe.go b/experiments/9_complex_signal/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/9_complex_signal/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/9_complex_signal/port.go b/experiments/9_complex_signal/port.go deleted file mode 100644 index 7ce0d4a..0000000 --- a/experiments/9_complex_signal/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(*AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/9_complex_signal/ports.go b/experiments/9_complex_signal/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/9_complex_signal/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/9_complex_signal/signal.go b/experiments/9_complex_signal/signal.go deleted file mode 100644 index c4caeb4..0000000 --- a/experiments/9_complex_signal/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetVal() any { - return s.val -} diff --git a/experiments/README.md b/experiments/README.md deleted file mode 100644 index ee4f01e..0000000 --- a/experiments/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains experiments made to reach a proof of concept of FMesh. Each next experiment is started as a copy of previous one. \ No newline at end of file diff --git a/export/dot/config.go b/export/dot/config.go new file mode 100644 index 0000000..c8589c6 --- /dev/null +++ b/export/dot/config.go @@ -0,0 +1,137 @@ +package dot + +import fmeshcomponent "github.com/hovsep/fmesh/component" + +type attributesMap map[string]string + +type ComponentConfig struct { + Subgraph attributesMap + SubgraphNodeBaseAttrs attributesMap + Node attributesMap + NodeDefaultLabel string + ErrorNode attributesMap + SubgraphAttributesByActivationResultCode map[fmeshcomponent.ActivationResultCode]attributesMap +} + +type PortConfig struct { + Node attributesMap +} + +type LegendConfig struct { + Subgraph attributesMap + Node attributesMap +} + +type PipeConfig struct { + Edge attributesMap +} + +type Config struct { + MainGraph attributesMap + Component ComponentConfig + Port PortConfig + Pipe PipeConfig + Legend LegendConfig +} + +var ( + defaultConfig = &Config{ + MainGraph: attributesMap{ + "layout": "dot", + "splines": "ortho", + }, + Component: ComponentConfig{ + Subgraph: attributesMap{ + "cluster": "true", + "style": "rounded", + "color": "black", + "margin": "20", + "penwidth": "5", + }, + SubgraphNodeBaseAttrs: attributesMap{ + "fontname": "Courier New", + "width": "1.0", + "height": "1.0", + "penwidth": "2.5", + "style": "filled", + }, + Node: attributesMap{ + "shape": "rect", + "color": "#9dddea", + "style": "filled", + }, + NodeDefaultLabel: "𝑓", + ErrorNode: nil, + SubgraphAttributesByActivationResultCode: map[fmeshcomponent.ActivationResultCode]attributesMap{ + fmeshcomponent.ActivationCodeOK: { + "color": "green", + }, + fmeshcomponent.ActivationCodeNoInput: { + "color": "yellow", + }, + fmeshcomponent.ActivationCodeNoFunction: { + "color": "gray", + }, + fmeshcomponent.ActivationCodeReturnedError: { + "color": "red", + }, + fmeshcomponent.ActivationCodePanicked: { + "color": "pink", + }, + fmeshcomponent.ActivationCodeWaitingForInputsClear: { + "color": "blue", + }, + fmeshcomponent.ActivationCodeWaitingForInputsKeep: { + "color": "purple", + }, + }, + }, + Port: PortConfig{ + Node: attributesMap{ + "shape": "circle", + }, + }, + Pipe: PipeConfig{ + Edge: attributesMap{ + "minlen": "3", + "penwidth": "2", + "color": "#e437ea", + }, + }, + Legend: LegendConfig{ + Subgraph: attributesMap{ + "style": "dashed,filled", + "fillcolor": "#e2c6fc", + }, + Node: attributesMap{ + "shape": "plaintext", + "color": "green", + "fontname": "Courier New", + }, + }, + } + + legendTemplate = ` + + {{ if .meshDescription }} + + + + {{ end }} + + {{ if .cycleNumber }} + + + + {{ end }} + + {{ if .stats }} + {{ range .stats }} + + + + {{ end }} + {{ end }} +
Description:{{ .meshDescription }}
Cycle:{{ .cycleNumber }}
{{ .Name }}:{{ .Value }}
+ ` +) diff --git a/export/dot/dot.go b/export/dot/dot.go new file mode 100644 index 0000000..3df40a8 --- /dev/null +++ b/export/dot/dot.go @@ -0,0 +1,339 @@ +package dot + +import ( + "bytes" + "fmt" + "github.com/hovsep/fmesh" + fmeshcomponent "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/cycle" + "github.com/hovsep/fmesh/export" + "github.com/hovsep/fmesh/port" + "github.com/lucasepe/dot" + "html/template" + "sort" +) + +type statEntry struct { + Name string + Value int +} + +type dotExporter struct { + config *Config +} + +const ( + nodeIDLabel = "export/dot/id" +) + +// NewDotExporter returns exporter with default configuration +func NewDotExporter() export.Exporter { + return NewDotExporterWithConfig(defaultConfig) +} + +// NewDotExporterWithConfig returns exporter with custom configuration +func NewDotExporterWithConfig(config *Config) export.Exporter { + return &dotExporter{ + config: config, + } +} + +// Export returns the f-mesh as DOT-graph +func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { + if fm.Components().Len() == 0 { + return nil, nil + } + + graph, err := d.buildGraph(fm, nil) + + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + graph.Write(buf) + + return buf.Bytes(), nil +} + +// ExportWithCycles returns multiple graphs showing the state of the given f-mesh in each activation cycle +func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Cycles) ([][]byte, error) { + if fm.Components().Len() == 0 { + return nil, nil + } + + if len(activationCycles) == 0 { + return nil, nil + } + + results := make([][]byte, len(activationCycles)) + + for _, activationCycle := range activationCycles { + graphForCycle, err := d.buildGraph(fm, activationCycle) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + graphForCycle.Write(buf) + + results[activationCycle.Number()-1] = buf.Bytes() + } + + return results, nil +} + +// buildGraph returns f-mesh as a graph +// activationCycle may be passed optionally to get a representation of f-mesh in a given activation cycle +func (d *dotExporter) buildGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle) (*dot.Graph, error) { + mainGraph, err := d.getMainGraph(fm, activationCycle) + if err != nil { + return nil, fmt.Errorf("failed to get main graph: %w", err) + } + + components, err := fm.Components().Components() + if err != nil { + return nil, fmt.Errorf("failed to get components: %w", err) + } + + err = d.addComponents(mainGraph, components, activationCycle) + if err != nil { + return nil, fmt.Errorf("failed to add components: %w", err) + } + + err = d.addPipes(mainGraph, components) + if err != nil { + return nil, fmt.Errorf("failed to add pipes: %w", err) + } + return mainGraph, nil +} + +// getMainGraph creates and returns the main (root) graph +func (d *dotExporter) getMainGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle) (*dot.Graph, error) { + graph := dot.NewGraph(dot.Directed) + + setAttrMap(&graph.AttributesMap, d.config.MainGraph) + + err := d.addLegend(graph, fm, activationCycle) + if err != nil { + return nil, fmt.Errorf("failed to build main graph: %w", err) + } + + return graph, nil +} + +// addPipes adds pipes representation to the graph +func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.ComponentsMap) error { + for _, c := range components { + srcPorts, err := c.Outputs().Ports() + if err != nil { + return err + } + + for _, srcPort := range srcPorts { + destPorts, err := srcPort.Pipes().Ports() + if err != nil { + return err + } + for _, destPort := range destPorts { + // Any destination port in any pipe is input port, but we do not know in which component + // so we use the label we added earlier + destPortID, err := destPort.Label(nodeIDLabel) + if err != nil { + return fmt.Errorf("failed to add pipe to port: %s : %w", destPort.Name(), err) + } + // Delete label, as it is not needed anymore + //destPort.DeleteLabel(nodeIDLabel) + + // Any source port in any pipe is always output port, so we can build its node ID + srcPortNode := graph.FindNodeByID(getPortID(c.Name(), port.DirectionOut, srcPort.Name())) + destPortNode := graph.FindNodeByID(destPortID) + + graph.Edge(srcPortNode, destPortNode, func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Pipe.Edge) + }) + } + } + } + return nil +} + +// addComponents adds components representation to the graph +func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent.ComponentsMap, activationCycle *cycle.Cycle) error { + for _, c := range components { + // Component + var activationResult *fmeshcomponent.ActivationResult + if activationCycle != nil { + activationResult = activationCycle.ActivationResults().ByComponentName(c.Name()) + } + componentSubgraph := d.getComponentSubgraph(graph, c, activationResult) + componentNode := d.getComponentNode(componentSubgraph, c, activationResult) + + // Input ports + inputPorts, err := c.Inputs().Ports() + if err != nil { + return err + } + for _, p := range inputPorts { + portNode := d.getPortNode(c, p, componentSubgraph) + componentSubgraph.Edge(portNode, componentNode) + } + + // Output ports + outputPorts, err := c.Outputs().Ports() + if err != nil { + return err + } + for _, p := range outputPorts { + portNode := d.getPortNode(c, p, componentSubgraph) + componentSubgraph.Edge(componentNode, portNode) + } + } + return nil +} + +// getPortNode creates and returns a node representing one port +func (d *dotExporter) getPortNode(c *fmeshcomponent.Component, p *port.Port, componentSubgraph *dot.Graph) *dot.Node { + portID := getPortID(c.Name(), p.LabelOrDefault(port.DirectionLabel, ""), p.Name()) + + //Mark ports to be able to find their respective nodes later when adding pipes + p.AddLabel(nodeIDLabel, portID) + + portNode := componentSubgraph.NodeWithID(portID, func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Port.Node) + a.Attr("label", p.Name()).Attr("group", c.Name()) + }) + + return portNode +} + +// getComponentSubgraph creates component subgraph and returns it +func (d *dotExporter) getComponentSubgraph(graph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Graph { + componentSubgraph := graph.NewSubgraph() + + setAttrMap(componentSubgraph.NodeBaseAttrs(), d.config.Component.SubgraphNodeBaseAttrs) + setAttrMap(&componentSubgraph.AttributesMap, d.config.Component.Subgraph) + + // Set cycle specific attributes + if activationResult != nil { + if attributesByCode, ok := d.config.Component.SubgraphAttributesByActivationResultCode[activationResult.Code()]; ok { + setAttrMap(&componentSubgraph.AttributesMap, attributesByCode) + } + } + + componentSubgraph.Attr("label", component.Name()) + + return componentSubgraph +} + +// getComponentNode creates component node and returns it +func (d *dotExporter) getComponentNode(componentSubgraph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Node { + componentNode := componentSubgraph.Node(func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Component.Node) + }) + + label := d.config.Component.NodeDefaultLabel + + if component.Description() != "" { + label = component.Description() + } + + if activationResult != nil { + if activationResult.ActivationError() != nil { + errorNode := componentSubgraph.Node(func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Component.ErrorNode) + }) + errorNode. + Attr("label", activationResult.ActivationError().Error()) + componentSubgraph.Edge(componentNode, errorNode) + } + } + + componentNode. + Attr("label", label). + Attr("group", component.Name()) + return componentNode +} + +// addLegend adds useful information about f-mesh and (optionally) current activation cycle +func (d *dotExporter) addLegend(graph *dot.Graph, fm *fmesh.FMesh, activationCycle *cycle.Cycle) error { + subgraph := graph.NewSubgraph() + + setAttrMap(&subgraph.AttributesMap, d.config.Legend.Subgraph) + subgraph.Attr("label", "Legend:") + + legendData := make(map[string]any) + legendData["meshDescription"] = fmt.Sprintf("This mesh consist of %d components", fm.Components().Len()) + if fm.Description() != "" { + legendData["meshDescription"] = fm.Description() + } + + if activationCycle != nil { + legendData["cycleNumber"] = activationCycle.Number() + legendData["stats"] = getCycleStats(activationCycle) + } + + legendHTML := new(bytes.Buffer) + err := template.Must( + template.New("legend"). + Parse(legendTemplate)). + Execute(legendHTML, legendData) + + if err != nil { + return fmt.Errorf("failed to render legend: %w", err) + } + + subgraph.Node(func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Legend.Node) + a.Attr("label", dot.HTML(legendHTML.String())) + }) + + return nil +} + +// getCycleStats returns basic cycle stats +func getCycleStats(activationCycle *cycle.Cycle) []*statEntry { + statsMap := map[string]*statEntry{ + // Number of activated must be shown always + "activated": { + Name: "Activated", + Value: 0, + }, + } + for _, ar := range activationCycle.ActivationResults() { + if ar.Activated() { + statsMap["activated"].Value++ + } + + if entryByCode, ok := statsMap[ar.Code().String()]; ok { + entryByCode.Value++ + } else { + statsMap[ar.Code().String()] = &statEntry{ + Name: ar.Code().String(), + Value: 1, + } + } + } + // Convert to slice to preserve keys order + statsList := make([]*statEntry, 0) + for _, entry := range statsMap { + statsList = append(statsList, entry) + } + + sort.Slice(statsList, func(i, j int) bool { + return statsList[i].Name < statsList[j].Name + }) + return statsList +} + +// getPortID returns unique ID used to locate ports while building pipe edges +func getPortID(componentName string, portDirection string, portName string) string { + return fmt.Sprintf("component/%s/%s/%s", componentName, portDirection, portName) +} + +// setAttrMap sets all attributes to target +func setAttrMap(target *dot.AttributesMap, attributes attributesMap) { + for attrName, attrValue := range attributes { + target.Attr(attrName, attrValue) + } +} diff --git a/export/dot/dot_test.go b/export/dot/dot_test.go new file mode 100644 index 0000000..9cf92dd --- /dev/null +++ b/export/dot/dot_test.go @@ -0,0 +1,156 @@ +package dot + +import ( + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_dotExporter_Export(t *testing.T) { + type args struct { + fm *fmesh.FMesh + } + tests := []struct { + name string + args args + assertions func(t *testing.T, data []byte, err error) + }{ + { + name: "empty f-mesh", + args: args{ + fm: fmesh.New("fm"), + }, + assertions: func(t *testing.T, data []byte, err error) { + assert.NoError(t, err) + assert.Empty(t, data) + }, + }, + { + name: "happy path", + args: args{ + fm: func() *fmesh.FMesh { + adder := component.New("adder"). + WithDescription("This component adds 2 numbers"). + WithInputs("num1", "num2"). + WithOutputs("result"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + //The activation func can be even empty, does not affect export + return nil + }) + + multiplier := component.New("multiplier"). + WithDescription("This component multiplies number by 3"). + WithInputs("num"). + WithOutputs("result"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + //The activation func can be even empty, does not affect export + return nil + }) + + adder.OutputByName("result").PipeTo(multiplier.InputByName("num")) + + fm := fmesh.New("fm"). + WithDescription("This f-mesh has just one component"). + WithComponents(adder, multiplier) + return fm + }(), + }, + assertions: func(t *testing.T, data []byte, err error) { + assert.NoError(t, err) + assert.NotEmpty(t, data) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exporter := NewDotExporter() + + got, err := exporter.Export(tt.args.fm) + if tt.assertions != nil { + tt.assertions(t, got, err) + } + }) + } +} + +func Test_dotExporter_ExportWithCycles(t *testing.T) { + type args struct { + fm *fmesh.FMesh + } + tests := []struct { + name string + args args + assertions func(t *testing.T, data [][]byte, err error) + }{ + { + name: "happy path", + args: args{ + fm: func() *fmesh.FMesh { + adder := component.New("adder"). + WithDescription("This component adds 2 numbers"). + WithInputs("num1", "num2"). + WithOutputs("result"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1, err := inputs.ByName("num1").FirstSignalPayload() + if err != nil { + return err + } + + num2, err := inputs.ByName("num2").FirstSignalPayload() + if err != nil { + return err + } + + outputs.ByName("result").PutSignals(signal.New(num1.(int) + num2.(int))) + return nil + }) + + multiplier := component.New("multiplier"). + WithDescription("This component multiplies number by 3"). + WithInputs("num"). + WithOutputs("result"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num, err := inputs.ByName("num").FirstSignalPayload() + if err != nil { + return err + } + outputs.ByName("result").PutSignals(signal.New(num.(int) * 3)) + return nil + }) + + adder.OutputByName("result").PipeTo(multiplier.InputByName("num")) + + fm := fmesh.New("fm"). + WithDescription("This f-mesh has just one component"). + WithComponents(adder, multiplier) + + adder.InputByName("num1").PutSignals(signal.New(15)) + adder.InputByName("num2").PutSignals(signal.New(12)) + + return fm + }(), + }, + assertions: func(t *testing.T, data [][]byte, err error) { + assert.NoError(t, err) + assert.NotEmpty(t, data) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + cycles, err := tt.args.fm.Run() + assert.NoError(t, err) + + exporter := NewDotExporter() + + got, err := exporter.ExportWithCycles(tt.args.fm, cycles) + if tt.assertions != nil { + tt.assertions(t, got, err) + } + }) + } +} diff --git a/export/exporter.go b/export/exporter.go new file mode 100644 index 0000000..8d114e7 --- /dev/null +++ b/export/exporter.go @@ -0,0 +1,15 @@ +package export + +import ( + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/cycle" +) + +// Exporter is the common interface for all formats +type Exporter interface { + // Export returns f-mesh representation in some format + Export(fm *fmesh.FMesh) ([]byte, error) + + // ExportWithCycles returns the f-mesh state representation in each activation cycle + ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Cycles) ([][]byte, error) +} diff --git a/fmesh.go b/fmesh.go index 7736097..02345e0 100644 --- a/fmesh.go +++ b/fmesh.go @@ -1,6 +1,9 @@ package fmesh import ( + "errors" + "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "sync" @@ -22,66 +25,94 @@ var defaultConfig = Config{ // FMesh is the functional mesh type FMesh struct { - name string - description string - components component.Collection - config Config + common.NamedEntity + common.DescribedEntity + *common.Chainable + components *component.Collection + cycles *cycle.Group + config Config } // New creates a new f-mesh func New(name string) *FMesh { return &FMesh{ - name: name, - components: component.NewCollection(), - config: defaultConfig, + NamedEntity: common.NewNamedEntity(name), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + cycles: cycle.NewGroup(), + config: defaultConfig, } } -// Name getter -func (fm *FMesh) Name() string { - return fm.name -} - -// Description getter -func (fm *FMesh) Description() string { - return fm.description -} - -func (fm *FMesh) Components() component.Collection { +// Components getter +func (fm *FMesh) Components() *component.Collection { + if fm.HasErr() { + return component.NewCollection().WithErr(fm.Err()) + } return fm.components } +//@TODO: add shortcut method: ComponentByName() + // WithDescription sets a description func (fm *FMesh) WithDescription(description string) *FMesh { - fm.description = description + if fm.HasErr() { + return fm + } + + fm.DescribedEntity = common.NewDescribedEntity(description) return fm } // WithComponents adds components to f-mesh func (fm *FMesh) WithComponents(components ...*component.Component) *FMesh { + if fm.HasErr() { + return fm + } + for _, c := range components { fm.components = fm.components.With(c) + if c.HasErr() { + return fm.WithErr(c.Err()) + } } return fm } // WithConfig sets the configuration and returns the f-mesh func (fm *FMesh) WithConfig(config Config) *FMesh { + if fm.HasErr() { + return fm + } + fm.config = config return fm } // runCycle runs one activation cycle (tries to activate ready components) -func (fm *FMesh) runCycle() *cycle.Cycle { - newCycle := cycle.New() +func (fm *FMesh) runCycle() { + newCycle := cycle.New().WithNumber(fm.cycles.Len() + 1) - if len(fm.components) == 0 { - return newCycle + if fm.HasErr() { + newCycle.SetErr(fm.Err()) + } + + if fm.Components().Len() == 0 { + newCycle.SetErr(errors.Join(errFailedToRunCycle, errNoComponents)) } var wg sync.WaitGroup - for _, c := range fm.components { + components, err := fm.Components().Components() + if err != nil { + newCycle.SetErr(errors.Join(errFailedToRunCycle, err)) + } + + for _, c := range components { + if c.HasErr() { + fm.SetErr(c.Err()) + } wg.Add(1) go func(component *component.Component, cycle *cycle.Cycle) { @@ -94,67 +125,153 @@ func (fm *FMesh) runCycle() *cycle.Cycle { } wg.Wait() - return newCycle + + //Bubble up chain errors from activation results + for _, ar := range newCycle.ActivationResults() { + if ar.HasErr() { + newCycle.SetErr(ar.Err()) + break + } + } + + if newCycle.HasErr() { + fm.SetErr(newCycle.Err()) + } + + fm.cycles = fm.cycles.With(newCycle) + + return } // DrainComponents drains the data from activated components -func (fm *FMesh) drainComponents(cycle *cycle.Cycle) { - for _, c := range fm.Components() { - activationResult := cycle.ActivationResults().ByComponentName(c.Name()) +func (fm *FMesh) drainComponents() { + if fm.HasErr() { + fm.SetErr(errors.Join(ErrFailedToDrain, fm.Err())) + return + } + + fm.clearInputs() + if fm.HasErr() { + return + } + + components, err := fm.Components().Components() + if err != nil { + fm.SetErr(errors.Join(ErrFailedToDrain, err)) + return + } + + lastCycle := fm.cycles.Last() + + for _, c := range components { + activationResult := lastCycle.ActivationResults().ByComponentName(c.Name()) + + if activationResult.HasErr() { + fm.SetErr(errors.Join(ErrFailedToDrain, activationResult.Err())) + return + } if !activationResult.Activated() { + // Component did not activate, so it did not create new output signals, hence nothing to drain continue } + // Components waiting for inputs are never drained if component.IsWaitingForInput(activationResult) { - if !component.WantsToKeepInputs(activationResult) { - c.ClearInputs() - } - // Components waiting for inputs are not flushed + // @TODO: maybe we should additionally clear outputs + // because it is technically possible to set some output signals and then return errWaitingForInput in AF continue } - // Normally components are fully drained c.FlushOutputs() + + } +} + +// clearInputs clears all the input ports of all components activated in latest cycle +func (fm *FMesh) clearInputs() { + if fm.HasErr() { + return + } + + components, err := fm.Components().Components() + if err != nil { + fm.SetErr(errors.Join(errFailedToClearInputs, err)) + return + } + + lastCycle := fm.cycles.Last() + + for _, c := range components { + activationResult := lastCycle.ActivationResults().ByComponentName(c.Name()) + + if activationResult.HasErr() { + fm.SetErr(errors.Join(errFailedToClearInputs, activationResult.Err())) + } + + if !activationResult.Activated() { + // Component did not activate hence it's inputs must be clear + continue + } + + if component.IsWaitingForInput(activationResult) && component.WantsToKeepInputs(activationResult) { + // Component want to keep inputs for the next cycle + //@TODO: add fine grained control on which ports to keep + continue + } + c.ClearInputs() } } // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) -func (fm *FMesh) Run() (cycle.Collection, error) { - allCycles := cycle.NewCollection() +func (fm *FMesh) Run() (cycle.Cycles, error) { + if fm.HasErr() { + return nil, fm.Err() + } + for { - cycleResult := fm.runCycle() - allCycles = allCycles.With(cycleResult) + fm.runCycle() - mustStop, err := fm.mustStop(cycleResult, len(allCycles)) - if mustStop { - return allCycles, err + if mustStop, err := fm.mustStop(); mustStop { + return fm.cycles.CyclesOrNil(), err } - fm.drainComponents(cycleResult) + fm.drainComponents() + if fm.HasErr() { + return nil, fm.Err() + } } } -func (fm *FMesh) mustStop(cycleResult *cycle.Cycle, cycleNum int) (bool, error) { - if (fm.config.CyclesLimit > 0) && (cycleNum > fm.config.CyclesLimit) { +// mustStop defines when f-mesh must stop (it always checks only last cycle) +func (fm *FMesh) mustStop() (bool, error) { + if fm.HasErr() { + return false, nil + } + + lastCycle := fm.cycles.Last() + + if (fm.config.CyclesLimit > 0) && (lastCycle.Number() > fm.config.CyclesLimit) { return true, ErrReachedMaxAllowedCycles } - //Check if we are done (no components activated during the cycle => all inputs are processed) - if !cycleResult.HasActivatedComponents() { + if !lastCycle.HasActivatedComponents() { + // Stop naturally (no components activated during the cycle => all inputs are processed) return true, nil } //Check if mesh must stop because of configured error handling strategy switch fm.config.ErrorHandlingStrategy { case StopOnFirstErrorOrPanic: - if cycleResult.HasErrors() || cycleResult.HasPanics() { - return true, ErrHitAnErrorOrPanic + if lastCycle.HasErrors() || lastCycle.HasPanics() { + //@TODO: add failing components names to error + return true, fmt.Errorf("%w, cycle # %d", ErrHitAnErrorOrPanic, lastCycle.Number()) } return false, nil case StopOnFirstPanic: - if cycleResult.HasPanics() { + // @TODO: add more context to error + if lastCycle.HasPanics() { return true, ErrHitAPanic } return false, nil @@ -164,3 +281,9 @@ func (fm *FMesh) mustStop(cycleResult *cycle.Cycle, cycleNum int) (bool, error) return true, ErrUnsupportedErrorHandlingStrategy } } + +// WithErr returns f-mesh with error +func (fm *FMesh) WithErr(err error) *FMesh { + fm.SetErr(err) + return fm +} diff --git a/fmesh_test.go b/fmesh_test.go index f773528..c97e21e 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -2,6 +2,8 @@ package fmesh import ( "errors" + "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "github.com/hovsep/fmesh/port" @@ -25,8 +27,12 @@ func TestNew(t *testing.T) { name: "", }, want: &FMesh{ - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity(""), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + cycles: cycle.NewGroup(), + config: defaultConfig, }, }, { @@ -35,9 +41,12 @@ func TestNew(t *testing.T) { name: "fm1", }, want: &FMesh{ - name: "fm1", - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + cycles: cycle.NewGroup(), + config: defaultConfig, }, }, } @@ -65,10 +74,12 @@ func TestFMesh_WithDescription(t *testing.T) { description: "", }, want: &FMesh{ - name: "fm1", - description: "", - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + cycles: cycle.NewGroup(), + config: defaultConfig, }, }, { @@ -78,10 +89,12 @@ func TestFMesh_WithDescription(t *testing.T) { description: "descr", }, want: &FMesh{ - name: "fm1", - description: "descr", - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity("descr"), + Chainable: common.NewChainable(), + components: component.NewCollection(), + cycles: cycle.NewGroup(), + config: defaultConfig, }, }, } @@ -112,8 +125,11 @@ func TestFMesh_WithConfig(t *testing.T) { }, }, want: &FMesh{ - name: "fm1", - components: component.Collection{}, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + cycles: cycle.NewGroup(), config: Config{ ErrorHandlingStrategy: IgnoreAll, CyclesLimit: 9999, @@ -133,10 +149,10 @@ func TestFMesh_WithComponents(t *testing.T) { components []*component.Component } tests := []struct { - name string - fm *FMesh - args args - wantComponents component.Collection + name string + fm *FMesh + args args + assertions func(t *testing.T, fm *FMesh) }{ { name: "no components", @@ -144,7 +160,9 @@ func TestFMesh_WithComponents(t *testing.T) { args: args{ components: nil, }, - wantComponents: component.Collection{}, + assertions: func(t *testing.T, fm *FMesh) { + assert.Zero(t, fm.Components().Len()) + }, }, { name: "with single component", @@ -154,8 +172,9 @@ func TestFMesh_WithComponents(t *testing.T) { component.New("c1"), }, }, - wantComponents: component.Collection{ - "c1": component.New("c1"), + assertions: func(t *testing.T, fm *FMesh) { + assert.Equal(t, fm.Components().Len(), 1) + assert.NotNil(t, fm.Components().ByName("c1")) }, }, { @@ -167,9 +186,10 @@ func TestFMesh_WithComponents(t *testing.T) { component.New("c2"), }, }, - wantComponents: component.Collection{ - "c1": component.New("c1"), - "c2": component.New("c2"), + assertions: func(t *testing.T, fm *FMesh) { + assert.Equal(t, fm.Components().Len(), 2) + assert.NotNil(t, fm.Components().ByName("c1")) + assert.NotNil(t, fm.Components().ByName("c2")) }, }, { @@ -179,68 +199,25 @@ func TestFMesh_WithComponents(t *testing.T) { components: []*component.Component{ component.New("c1").WithDescription("descr1"), component.New("c2").WithDescription("descr2"), - component.New("c2").WithDescription("descr3"), //This will overwrite the previous one + component.New("c2").WithDescription("new descr for c2"), //This will overwrite the previous one component.New("c4").WithDescription("descr4"), }, }, - wantComponents: component.Collection{ - "c1": component.New("c1").WithDescription("descr1"), - "c2": component.New("c2").WithDescription("descr3"), - "c4": component.New("c4").WithDescription("descr4"), + assertions: func(t *testing.T, fm *FMesh) { + assert.Equal(t, fm.Components().Len(), 3) + assert.NotNil(t, fm.Components().ByName("c1")) + assert.NotNil(t, fm.Components().ByName("c2")) + assert.NotNil(t, fm.Components().ByName("c4")) + assert.Equal(t, "new descr for c2", fm.Components().ByName("c2").Description()) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.wantComponents, tt.fm.WithComponents(tt.args.components...).Components()) - }) - } -} - -func TestFMesh_Name(t *testing.T) { - tests := []struct { - name string - fm *FMesh - want string - }{ - { - name: "empty name is valid", - fm: New(""), - want: "", - }, - { - name: "with name", - fm: New("fm1"), - want: "fm1", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.fm.Name()) - }) - } -} - -func TestFMesh_Description(t *testing.T) { - tests := []struct { - name string - fm *FMesh - want string - }{ - { - name: "empty description", - fm: New("fm1"), - want: "", - }, - { - name: "with description", - fm: New("fm1").WithDescription("descr"), - want: "descr", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.fm.Description()) + fmAfter := tt.fm.WithComponents(tt.args.components...) + if tt.assertions != nil { + tt.assertions(t, fmAfter) + } }) } } @@ -250,14 +227,14 @@ func TestFMesh_Run(t *testing.T) { name string fm *FMesh initFM func(fm *FMesh) - want cycle.Collection + want cycle.Cycles wantErr bool }{ { name: "empty mesh stops after first cycle", fm: New("fm"), - want: cycle.NewCollection().With(cycle.New()), - wantErr: false, + want: nil, + wantErr: true, }, { name: "unsupported error handling strategy", @@ -270,21 +247,21 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component simply puts a constant on o1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(77)) return nil }), ), initFM: func(fm *FMesh) { //Fire the mesh - fm.Components().ByName("c1").Inputs().ByName("i1").PutSignals(signal.New("start c1")) + fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New("start c1")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( cycle.New(). WithActivationResults(component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK)), - ), + ).CyclesOrNil(), wantErr: true, }, { @@ -297,21 +274,21 @@ func TestFMesh_Run(t *testing.T) { component.New("c1"). WithDescription("This component just returns an unexpected error"). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("boom") })), initFM: func(fm *FMesh) { - fm.Components().ByName("c1").Inputs().ByName("i1").PutSignals(signal.New("start")) + fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New("start")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( cycle.New(). WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("component returned an error: boom")), + WithActivationError(errors.New("component returned an error: boom")), ), - ), + ).CyclesOrNil(), wantErr: true, }, { @@ -325,7 +302,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component just sends a number to c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(10)) return nil }), @@ -333,7 +310,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c1 and passes it to c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), @@ -341,14 +318,14 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component returns an error, but the mesh is configured to ignore errors"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("boom") }), component.New("c4"). WithDescription("This component receives a number from c2 and panics"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { panic("no way") return nil }), @@ -356,14 +333,14 @@ func TestFMesh_Run(t *testing.T) { initFM: func(fm *FMesh) { c1, c2, c3, c4 := fm.Components().ByName("c1"), fm.Components().ByName("c2"), fm.Components().ByName("c3"), fm.Components().ByName("c4") //Piping - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) - c2.Outputs().ByName("o1").PipeTo(c4.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + c2.OutputByName("o1").PipeTo(c4.InputByName("i1")) //Input data - c1.Inputs().ByName("i1").PutSignals(signal.New("start c1")) - c3.Inputs().ByName("i1").PutSignals(signal.New("start c3")) + c1.InputByName("i1").PutSignals(signal.New("start c1")) + c3.InputByName("i1").PutSignals(signal.New("start c3")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( cycle.New(). WithActivationResults( component.NewActivationResult("c1"). @@ -375,7 +352,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c3"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("component returned an error: boom")), + WithActivationError(errors.New("component returned an error: boom")), component.NewActivationResult("c4"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -409,9 +386,9 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c4"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). - WithError(errors.New("panicked with: no way")), + WithActivationError(errors.New("panicked with: no way")), ), - ), + ).CyclesOrNil(), wantErr: true, }, { @@ -425,7 +402,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component just sends a number to c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(10)) return nil }), @@ -433,7 +410,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c1 and passes it to c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), @@ -441,14 +418,14 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component returns an error, but the mesh is configured to ignore errors"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("boom") }), component.New("c4"). WithDescription("This component receives a number from c2 and panics, but the mesh is configured to ignore even panics"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) // Even component panicked, it managed to set some data on output "o1" @@ -460,7 +437,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), @@ -468,15 +445,15 @@ func TestFMesh_Run(t *testing.T) { initFM: func(fm *FMesh) { c1, c2, c3, c4, c5 := fm.Components().ByName("c1"), fm.Components().ByName("c2"), fm.Components().ByName("c3"), fm.Components().ByName("c4"), fm.Components().ByName("c5") //Piping - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) - c2.Outputs().ByName("o1").PipeTo(c4.Inputs().ByName("i1")) - c4.Outputs().ByName("o1").PipeTo(c5.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + c2.OutputByName("o1").PipeTo(c4.InputByName("i1")) + c4.OutputByName("o1").PipeTo(c5.InputByName("i1")) //Input data - c1.Inputs().ByName("i1").PutSignals(signal.New("start c1")) - c3.Inputs().ByName("i1").PutSignals(signal.New("start c3")) + c1.InputByName("i1").PutSignals(signal.New("start c1")) + c3.InputByName("i1").PutSignals(signal.New("start c3")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( //c1 and c3 activated, c3 finishes with error cycle.New(). WithActivationResults( @@ -489,7 +466,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c3"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("component returned an error: boom")), + WithActivationError(errors.New("component returned an error: boom")), component.NewActivationResult("c4"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -531,7 +508,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c4"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). - WithError(errors.New("panicked with: no way")), + WithActivationError(errors.New("panicked with: no way")), component.NewActivationResult("c5"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -574,7 +551,7 @@ func TestFMesh_Run(t *testing.T) { SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), ), - ), + ).CyclesOrNil(), wantErr: false, }, } @@ -602,7 +579,7 @@ func TestFMesh_Run(t *testing.T) { assert.Equal(t, tt.want[i].ActivationResults()[componentName].Code(), gotActivationResult.Code()) if tt.want[i].ActivationResults()[componentName].IsError() { - assert.EqualError(t, tt.want[i].ActivationResults()[componentName].Error(), gotActivationResult.Error().Error()) + assert.EqualError(t, tt.want[i].ActivationResults()[componentName].ActivationError(), gotActivationResult.ActivationError().Error()) } else { assert.False(t, gotActivationResult.IsError()) } @@ -614,15 +591,17 @@ func TestFMesh_Run(t *testing.T) { func TestFMesh_runCycle(t *testing.T) { tests := []struct { - name string - fm *FMesh - initFM func(fm *FMesh) - want *cycle.Cycle + name string + fm *FMesh + initFM func(fm *FMesh) + want *cycle.Cycle + wantError bool }{ { - name: "empty mesh", - fm: New("empty mesh"), - want: cycle.New(), + name: "empty mesh", + fm: New("empty mesh"), + want: nil, + wantError: true, }, { name: "all components activated in one cycle (concurrently)", @@ -630,7 +609,7 @@ func TestFMesh_runCycle(t *testing.T) { component.New("c1"). WithDescription(""). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { // No output return nil }), @@ -638,25 +617,25 @@ func TestFMesh_runCycle(t *testing.T) { WithDescription(""). WithInputs("i1"). WithOutputs("o1", "o2"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { // Sets output outputs.ByName("o1").PutSignals(signal.New(1)) - outputs.ByName("o2").PutSignals(signal.NewGroup(2, 3, 4, 5)...) + outputs.ByName("o2").PutSignals(signal.NewGroup(2, 3, 4, 5).SignalsOrNil()...) return nil }), component.New("c3"). WithDescription(""). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { // No output return nil }), ), initFM: func(fm *FMesh) { - fm.Components().ByName("c1").Inputs().ByName("i1").PutSignals(signal.New(1)) - fm.Components().ByName("c2").Inputs().ByName("i1").PutSignals(signal.New(2)) - fm.Components().ByName("c3").Inputs().ByName("i1").PutSignals(signal.New(3)) + fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New(1)) + fm.Components().ByName("c2").InputByName("i1").PutSignals(signal.New(2)) + fm.Components().ByName("c3").InputByName("i1").PutSignals(signal.New(3)) }, want: cycle.New().WithActivationResults( component.NewActivationResult("c1"). @@ -668,7 +647,7 @@ func TestFMesh_runCycle(t *testing.T) { component.NewActivationResult("c3"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ), + ).WithNumber(1), }, } for _, tt := range tests { @@ -676,96 +655,107 @@ func TestFMesh_runCycle(t *testing.T) { if tt.initFM != nil { tt.initFM(tt.fm) } - assert.Equal(t, tt.want, tt.fm.runCycle()) + tt.fm.runCycle() + gotCycleResult := tt.fm.cycles.Last() + if tt.wantError { + assert.True(t, gotCycleResult.HasErr()) + assert.Error(t, gotCycleResult.Err()) + } else { + assert.False(t, gotCycleResult.HasErr()) + assert.NoError(t, gotCycleResult.Err()) + assert.Equal(t, tt.want, gotCycleResult) + } }) } } func TestFMesh_mustStop(t *testing.T) { - type args struct { - cycleResult *cycle.Cycle - cycleNum int - } tests := []struct { - name string - fmesh *FMesh - args args - want bool - wantErr error + name string + getFMesh func() *FMesh + want bool + wantErr error }{ { - name: "with default config, no time to stop", - fmesh: New("fm"), - args: args{ - cycleResult: cycle.New().WithActivationResults( + name: "with default config, no time to stop", + getFMesh: func() *FMesh { + fm := New("fm") + + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ), - cycleNum: 5, + ).WithNumber(5) + + fm.cycles = fm.cycles.With(c) + return fm }, want: false, wantErr: nil, }, { - name: "with default config, reached max cycles", - fmesh: New("fm"), - args: args{ - cycleResult: cycle.New().WithActivationResults( + name: "with default config, reached max cycles", + getFMesh: func() *FMesh { + fm := New("fm") + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ), - cycleNum: 1001, + ).WithNumber(1001) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, wantErr: ErrReachedMaxAllowedCycles, }, { - name: "mesh finished naturally and must stop", - fmesh: New("fm"), - args: args{ - cycleResult: cycle.New().WithActivationResults( + name: "mesh finished naturally and must stop", + getFMesh: func() *FMesh { + fm := New("fm") + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), - ), - cycleNum: 5, + ).WithNumber(5) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, wantErr: nil, }, { name: "mesh hit an error", - fmesh: New("fm").WithConfig(Config{ - ErrorHandlingStrategy: StopOnFirstErrorOrPanic, - CyclesLimit: UnlimitedCycles, - }), - args: args{ - cycleResult: cycle.New().WithActivationResults( + getFMesh: func() *FMesh { + fm := New("fm").WithConfig(Config{ + ErrorHandlingStrategy: StopOnFirstErrorOrPanic, + CyclesLimit: UnlimitedCycles, + }) + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("c1 activation finished with error")), - ), - cycleNum: 5, + WithActivationError(errors.New("c1 activation finished with error")), + ).WithNumber(5) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, - wantErr: ErrHitAnErrorOrPanic, + wantErr: fmt.Errorf("%w, cycle # %d", ErrHitAnErrorOrPanic, 5), }, { name: "mesh hit a panic", - fmesh: New("fm").WithConfig(Config{ - ErrorHandlingStrategy: StopOnFirstPanic, - }), - args: args{ - cycleResult: cycle.New().WithActivationResults( + getFMesh: func() *FMesh { + fm := New("fm").WithConfig(Config{ + ErrorHandlingStrategy: StopOnFirstPanic, + }) + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). - WithError(errors.New("c1 panicked")), - ), - cycleNum: 5, + WithActivationError(errors.New("c1 panicked")), + ).WithNumber(5) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, wantErr: ErrHitAPanic, @@ -773,14 +763,217 @@ func TestFMesh_mustStop(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.fmesh.mustStop(tt.args.cycleResult, tt.args.cycleNum) + fm := tt.getFMesh() + got, stopErr := fm.mustStop() if tt.wantErr != nil { - assert.EqualError(t, err, tt.wantErr.Error()) + assert.EqualError(t, stopErr, tt.wantErr.Error()) } else { - assert.NoError(t, err) + assert.NoError(t, stopErr) } assert.Equal(t, tt.want, got) }) } } + +func TO_BE_REWRITTEN_FMesh_drainComponents(t *testing.T) { + type args struct { + cycle *cycle.Cycle + } + tests := []struct { + name string + getFM func() *FMesh + args args + assertions func(t *testing.T, fm *FMesh) + }{ + { + name: "component not activated", + getFM: func() *FMesh { + fm := New("fm"). + WithComponents( + component.New("c1"). + WithDescription("This component has no activation function"). + WithInputs("i1"). + WithOutputs("o1")) + + fm.Components(). + ByName("c1"). + Inputs(). + ByName("i1"). + PutSignals(signal.New("input signal")) + + return fm + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput)), + }, + assertions: func(t *testing.T, fm *FMesh) { + // Assert that inputs are not cleared + assert.True(t, fm.Components().ByName("c1").InputByName("i1").HasSignals()) + }, + }, + { + name: "component fully drained", + getFM: func() *FMesh { + c1 := component.New("c1"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return nil + }) + + c2 := component.New("c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return nil + }) + + // Pipe + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + + // Simulate activation of c1 + c1.OutputByName("o1").PutSignals(signal.New("this signal is generated by c1")) + + return New("fm").WithComponents(c1, c2) + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput)), + }, + assertions: func(t *testing.T, fm *FMesh) { + // c1 output is cleared + assert.False(t, fm.Components().ByName("c1").OutputByName("o1").HasSignals()) + + // c2 input received flushed signal + assert.True(t, fm.Components().ByName("c2").InputByName("i1").HasSignals()) + sig, err := fm.Components().ByName("c2").InputByName("i1").FirstSignalPayload() + assert.NoError(t, err) + assert.Equal(t, "this signal is generated by c1", sig.(string)) + }, + }, + { + name: "component is waiting for inputs", + getFM: func() *FMesh { + + c1 := component.New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return nil + }) + + c2 := component.New("c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return nil + }) + + // Pipe + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + + // Simulate activation of c1 + // NOTE: normally component should not create any output signal if it is waiting for inputs + // but technically there is no limitation to do that and then return the special error to wait for inputs. + // F-mesh just never flushes components waiting for inputs, so this test checks that + c1.OutputByName("o1").PutSignals(signal.New("this signal is generated by c1")) + + // Also simulate input signal on one port + c1.InputByName("i1").PutSignals(signal.New("this is input signal for c1")) + + return New("fm").WithComponents(c1, c2) + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeWaitingForInputsClear). + WithActivationError(component.NewErrWaitForInputs(false)), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + }, + assertions: func(t *testing.T, fm *FMesh) { + // As c1 is waiting for inputs it's outputs must not be flushed + assert.False(t, fm.Components().ByName("c2").InputByName("i1").HasSignals()) + + // The inputs must be cleared + assert.False(t, fm.Components().ByName("c1").Inputs().AnyHasSignals()) + }, + }, + { + name: "component is waiting for inputs and wants to keep input signals", + getFM: func() *FMesh { + + c1 := component.New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return nil + }) + + c2 := component.New("c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return nil + }) + + // Pipe + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + + // Simulate activation of c1 + // NOTE: normally component should not create any output signal if it is waiting for inputs + // but technically there is no limitation to do that and then return the special error to wait for inputs. + // F-mesh just never flushes components waiting for inputs, so this test checks that + c1.OutputByName("o1").PutSignals(signal.New("this signal is generated by c1")) + + // Also simulate input signal on one port + c1.InputByName("i1").PutSignals(signal.New("this is input signal for c1")) + + return New("fm").WithComponents(c1, c2) + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeWaitingForInputsKeep). + WithActivationError(component.NewErrWaitForInputs(true)), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + }, + assertions: func(t *testing.T, fm *FMesh) { + // As c1 is waiting for inputs it's outputs must not be flushed + assert.False(t, fm.Components().ByName("c2").InputByName("i1").HasSignals()) + + // The inputs must NOT be cleared + assert.True(t, fm.Components().ByName("c1").Inputs().AnyHasSignals()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fm := tt.getFM() + fm.drainComponents() + if tt.assertions != nil { + tt.assertions(t, fm) + } + }) + } +} diff --git a/go.mod b/go.mod index b38dabe..ec2fd61 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/lucasepe/dot v0.4.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 60ce688..c22838b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/lucasepe/dot v0.4.3 h1:gqQaH00pPlyK21GgCtoYI70pwOsrjsjJFBgegYm2+/E= +github.com/lucasepe/dot v0.4.3/go.mod h1:5gEWjskJdc7e0jMJJLg/PVNv5ynTLvdTYq9OTgphX8Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/integration_tests/computation/basic_test.go b/integration_tests/computation/basic_test.go new file mode 100644 index 0000000..e675b1b --- /dev/null +++ b/integration_tests/computation/basic_test.go @@ -0,0 +1,127 @@ +package integration_tests + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/cycle" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "github.com/stretchr/testify/assert" + "os" + "strings" + "testing" +) + +func Test_Math(t *testing.T) { + tests := []struct { + name string + setupFM func() *fmesh.FMesh + setInputs func(fm *fmesh.FMesh) + assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) + }{ + { + name: "add and multiply", + setupFM: func() *fmesh.FMesh { + c1 := component.New("c1"). + WithDescription("adds 2 to the input"). + WithInputs("num"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num := inputs.ByName("num").FirstSignalPayloadOrNil() + outputs.ByName("res").PutSignals(signal.New(num.(int) + 2)) + return nil + }) + + c2 := component.New("c2"). + WithDescription("multiplies by 3"). + WithInputs("num"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num := inputs.ByName("num").FirstSignalPayloadOrDefault(0) + outputs.ByName("res").PutSignals(signal.New(num.(int) * 3)) + return nil + }) + + c1.OutputByName("res").PipeTo(c2.InputByName("num")) + return fmesh.New("fm").WithComponents(c1, c2).WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) + }, + setInputs: func(fm *fmesh.FMesh) { + fm.Components().ByName("c1").InputByName("num").PutSignals(signal.New(32)) + }, + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { + assert.NoError(t, err) + assert.Len(t, cycles, 3) + + resultSignals := fm.Components().ByName("c2").OutputByName("res").Buffer() + sig, err := resultSignals.FirstPayload() + assert.NoError(t, err) + assert.Len(t, resultSignals.SignalsOrNil(), 1) + assert.Equal(t, 102, sig.(int)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fm := tt.setupFM() + tt.setInputs(fm) + cycles, err := fm.Run() + tt.assertions(t, fm, cycles, err) + }) + } +} + +func Test_Readme(t *testing.T) { + t.Run("readme test", func(t *testing.T) { + fm := fmesh.New("hello world"). + WithComponents( + component.New("concat"). + WithInputs("i1", "i2"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + word1 := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + word2 := inputs.ByName("i2").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(word1 + word2)) + return nil + }), + component.New("case"). + WithInputs("i1"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputString := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(strings.ToTitle(inputString))) + return nil + })). + WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) + + fm.Components().ByName("concat").Outputs().ByName("res").PipeTo( + fm.Components().ByName("case").Inputs().ByName("i1"), + ) + + // Init inputs + fm.Components().ByName("concat").Inputs().ByName("i1").PutSignals(signal.New("hello ")) + fm.Components().ByName("concat").Inputs().ByName("i2").PutSignals(signal.New("world !")) + + // Run the mesh + _, err := fm.Run() + + // Check for errors + if err != nil { + fmt.Println("F-Mesh returned an error") + os.Exit(1) + } + + //Extract results + results := fm.Components().ByName("case").Outputs().ByName("res").FirstSignalPayloadOrNil() + fmt.Printf("Result is :%v", results) + assert.Equal(t, "HELLO WORLD !", results) + }) +} diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go deleted file mode 100644 index 38c5fce..0000000 --- a/integration_tests/computation/math_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package integration_tests - -import ( - "github.com/hovsep/fmesh" - "github.com/hovsep/fmesh/component" - "github.com/hovsep/fmesh/cycle" - "github.com/hovsep/fmesh/port" - "github.com/hovsep/fmesh/signal" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_Math(t *testing.T) { - tests := []struct { - name string - setupFM func() *fmesh.FMesh - setInputs func(fm *fmesh.FMesh) - assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) - }{ - { - name: "add and multiply", - setupFM: func() *fmesh.FMesh { - c1 := component.New("c1"). - WithDescription("adds 2 to the input"). - WithInputs("num"). - WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num := inputs.ByName("num").Signals().FirstPayload().(int) - outputs.ByName("res").PutSignals(signal.New(num + 2)) - return nil - }) - - c2 := component.New("c2"). - WithDescription("multiplies by 3"). - WithInputs("num"). - WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num := inputs.ByName("num").Signals().FirstPayload().(int) - outputs.ByName("res").PutSignals(signal.New(num * 3)) - return nil - }) - - c1.Outputs().ByName("res").PipeTo(c2.Inputs().ByName("num")) - - return fmesh.New("fm").WithComponents(c1, c2).WithConfig(fmesh.Config{ - ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, - CyclesLimit: 10, - }) - }, - setInputs: func(fm *fmesh.FMesh) { - fm.Components().ByName("c1").Inputs().ByName("num").PutSignals(signal.New(32)) - }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { - assert.NoError(t, err) - assert.Len(t, cycles, 3) - - resultSignals := fm.Components().ByName("c2").Outputs().ByName("res").Signals() - assert.Len(t, resultSignals, 1) - assert.Equal(t, 102, resultSignals.FirstPayload().(int)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fm := tt.setupFM() - tt.setInputs(fm) - cycles, err := fm.Run() - tt.assertions(t, fm, cycles, err) - }) - } -} diff --git a/integration_tests/error_handling/chainable_api_test.go b/integration_tests/error_handling/chainable_api_test.go new file mode 100644 index 0000000..64b4179 --- /dev/null +++ b/integration_tests/error_handling/chainable_api_test.go @@ -0,0 +1,155 @@ +package error_handling + +import ( + "errors" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Signal(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "no errors", + test: func(t *testing.T) { + sig := signal.New(123) + _, err := sig.Payload() + assert.False(t, sig.HasErr()) + assert.NoError(t, err) + + _ = sig.PayloadOrDefault(555) + assert.False(t, sig.HasErr()) + + _ = sig.PayloadOrNil() + assert.False(t, sig.HasErr()) + }, + }, + { + name: "error propagated from group to signal", + test: func(t *testing.T) { + emptyGroup := signal.NewGroup() + + sig := emptyGroup.First() + assert.True(t, sig.HasErr()) + assert.Error(t, sig.Err()) + + _, err := sig.Payload() + assert.Error(t, err) + assert.EqualError(t, err, signal.ErrNoSignalsInGroup.Error()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + +func Test_FMesh(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "no errors", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1").WithInputs("num1", "num2"). + WithOutputs("sum").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }), + ) + + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10)) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.False(t, fm.HasErr()) + assert.NoError(t, err) + }, + }, + { + name: "error propagated from component", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1"). + WithInputs("num1", "num2"). + WithOutputs("sum"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }). + WithErr(errors.New("some error in component")), + ) + + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10)) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.True(t, fm.HasErr()) + assert.Error(t, err) + assert.EqualError(t, err, "some error in component") + }, + }, + { + name: "error propagated from port", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1"). + WithInputs("num1", "num2"). + WithOutputs("sum"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }), + ) + + //Trying to search port by wrong name must lead to error which will bubble up at f-mesh level + fm.Components().ByName("c1").InputByName("num777").PutSignals(signal.New(10)) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.True(t, fm.HasErr()) + assert.Error(t, err) + assert.ErrorContains(t, err, "port not found, port name: num777") + }, + }, + { + name: "error propagated from signal", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1").WithInputs("num1", "num2"). + WithOutputs("sum").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }), + ) + + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10).WithErr(errors.New("some error in input signal"))) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.True(t, fm.HasErr()) + assert.Error(t, err) + assert.ErrorContains(t, err, "some error in input signal") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index b45fe19..6ad6126 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -17,7 +17,7 @@ func Test_Fan(t *testing.T) { name string setupFM func() *fmesh.FMesh setInputs func(fm *fmesh.FMesh) - assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) + assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) }{ { name: "fan-out (3 pipes from 1 source port)", @@ -26,7 +26,7 @@ func Test_Fan(t *testing.T) { component.New("producer"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(time.Now())) return nil }), @@ -34,7 +34,7 @@ func Test_Fan(t *testing.T) { component.New("consumer1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass received signal to output port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil @@ -43,7 +43,7 @@ func Test_Fan(t *testing.T) { component.New("consumer2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass received signal to output port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil @@ -52,35 +52,40 @@ func Test_Fan(t *testing.T) { component.New("consumer3"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass received signal to output port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), ) - fm.Components().ByName("producer").Outputs().ByName("o1").PipeTo( - fm.Components().ByName("consumer1").Inputs().ByName("i1"), - fm.Components().ByName("consumer2").Inputs().ByName("i1"), - fm.Components().ByName("consumer3").Inputs().ByName("i1")) + fm.Components().ByName("producer").OutputByName("o1").PipeTo( + fm.Components().ByName("consumer1").InputByName("i1"), + fm.Components().ByName("consumer2").InputByName("i1"), + fm.Components().ByName("consumer3").InputByName("i1")) return fm }, setInputs: func(fm *fmesh.FMesh) { //Fire the mesh - fm.Components().ByName("producer").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer").InputByName("start").PutSignals(signal.New(struct{}{})) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { //All consumers received a signal c1, c2, c3 := fm.Components().ByName("consumer1"), fm.Components().ByName("consumer2"), fm.Components().ByName("consumer3") - assert.True(t, c1.Outputs().ByName("o1").HasSignals()) - assert.True(t, c2.Outputs().ByName("o1").HasSignals()) - assert.True(t, c3.Outputs().ByName("o1").HasSignals()) + assert.True(t, c1.OutputByName("o1").HasSignals()) + assert.True(t, c2.OutputByName("o1").HasSignals()) + assert.True(t, c3.OutputByName("o1").HasSignals()) //All 3 signals are the same (literally the same address in memory) - sig1, sig2, sig3 := c1.Outputs().ByName("o1").Signals(), c2.Outputs().ByName("o1").Signals(), c3.Outputs().ByName("o1").Signals() - assert.Equal(t, sig1.FirstPayload(), sig2.FirstPayload()) - assert.Equal(t, sig2.FirstPayload(), sig3.FirstPayload()) + sig1, err := c1.OutputByName("o1").FirstSignalPayload() + assert.NoError(t, err) + sig2, err := c2.OutputByName("o1").FirstSignalPayload() + assert.NoError(t, err) + sig3, err := c3.OutputByName("o1").FirstSignalPayload() + assert.NoError(t, err) + assert.Equal(t, sig1, sig2) + assert.Equal(t, sig2, sig3) }, }, { @@ -89,7 +94,7 @@ func Test_Fan(t *testing.T) { producer1 := component.New("producer1"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(rand.Int())) return nil }) @@ -97,7 +102,7 @@ func Test_Fan(t *testing.T) { producer2 := component.New("producer2"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(rand.Int())) return nil }) @@ -105,42 +110,49 @@ func Test_Fan(t *testing.T) { producer3 := component.New("producer3"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(rand.Int())) return nil }) consumer := component.New("consumer"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }) - producer1.Outputs().ByName("o1").PipeTo(consumer.Inputs().ByName("i1")) - producer2.Outputs().ByName("o1").PipeTo(consumer.Inputs().ByName("i1")) - producer3.Outputs().ByName("o1").PipeTo(consumer.Inputs().ByName("i1")) + producer1.OutputByName("o1").PipeTo(consumer.InputByName("i1")) + producer2.OutputByName("o1").PipeTo(consumer.InputByName("i1")) + producer3.OutputByName("o1").PipeTo(consumer.InputByName("i1")) return fmesh.New("multiplexer").WithComponents(producer1, producer2, producer3, consumer) }, setInputs: func(fm *fmesh.FMesh) { - fm.Components().ByName("producer1").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) - fm.Components().ByName("producer2").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) - fm.Components().ByName("producer3").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer1").InputByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer2").InputByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer3").InputByName("start").PutSignals(signal.New(struct{}{})) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { assert.NoError(t, err) //Consumer received a signal - assert.True(t, fm.Components().ByName("consumer").Outputs().ByName("o1").HasSignals()) + assert.True(t, fm.Components().ByName("consumer").OutputByName("o1").HasSignals()) //The signal is combined and consist of 3 payloads - resultSignals := fm.Components().ByName("consumer").Outputs().ByName("o1").Signals() - assert.Len(t, resultSignals, 3) + resultSignals := fm.Components().ByName("consumer").OutputByName("o1").Buffer() + assert.Len(t, resultSignals.SignalsOrNil(), 3) //And they are all different - assert.NotEqual(t, resultSignals.FirstPayload(), resultSignals[1].Payload()) - assert.NotEqual(t, resultSignals[1].Payload(), resultSignals[2].Payload()) + sig0, err := resultSignals.FirstPayload() + assert.NoError(t, err) + sig1, err := resultSignals.SignalsOrNil()[1].Payload() + assert.NoError(t, err) + sig2, err := resultSignals.SignalsOrNil()[2].Payload() + assert.NoError(t, err) + + assert.NotEqual(t, sig0, sig1) + assert.NotEqual(t, sig1, sig2) }, }, } diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index 9c790c2..424428d 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -15,7 +15,7 @@ func Test_WaitingForInputs(t *testing.T) { name string setupFM func() *fmesh.FMesh setInputs func(fm *fmesh.FMesh) - assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) + assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) }{ { name: "waiting for longer chain", @@ -25,9 +25,10 @@ func Test_WaitingForInputs(t *testing.T) { WithDescription("This component just doubles the input"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - inputNum := inputs.ByName("i1").Signals().FirstPayload().(int) - outputs.ByName("o1").PutSignals(signal.New(inputNum * 2)) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputNum := inputs.ByName("i1").FirstSignalPayloadOrDefault(0) + + outputs.ByName("o1").PutSignals(signal.New(inputNum.(int) * 2)) return nil }) } @@ -42,27 +43,28 @@ func Test_WaitingForInputs(t *testing.T) { WithDescription("This component just sums 2 inputs"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { if !inputs.ByNames("i1", "i2").AllHaveSignals() { return component.NewErrWaitForInputs(true) } - inputNum1 := inputs.ByName("i1").Signals().FirstPayload().(int) - inputNum2 := inputs.ByName("i2").Signals().FirstPayload().(int) - outputs.ByName("o1").PutSignals(signal.New(inputNum1 + inputNum2)) + inputNum1 := inputs.ByName("i1").FirstSignalPayloadOrDefault(0) + inputNum2 := inputs.ByName("i2").FirstSignalPayloadOrDefault(0) + + outputs.ByName("o1").PutSignals(signal.New(inputNum1.(int) + inputNum2.(int))) return nil }) //This chain consist of 3 components: d1->d2->d3 - d1.Outputs().ByName("o1").PipeTo(d2.Inputs().ByName("i1")) - d2.Outputs().ByName("o1").PipeTo(d3.Inputs().ByName("i1")) + d1.OutputByName("o1").PipeTo(d2.InputByName("i1")) + d2.OutputByName("o1").PipeTo(d3.InputByName("i1")) //This chain has only 2: d4->d5 - d4.Outputs().ByName("o1").PipeTo(d5.Inputs().ByName("i1")) + d4.OutputByName("o1").PipeTo(d5.InputByName("i1")) //Both chains go into summator - d3.Outputs().ByName("o1").PipeTo(s.Inputs().ByName("i1")) - d5.Outputs().ByName("o1").PipeTo(s.Inputs().ByName("i2")) + d3.OutputByName("o1").PipeTo(s.InputByName("i1")) + d5.OutputByName("o1").PipeTo(s.InputByName("i2")) return fmesh.New("fm"). WithComponents(d1, d2, d3, d4, d5, s). @@ -74,13 +76,14 @@ func Test_WaitingForInputs(t *testing.T) { }, setInputs: func(fm *fmesh.FMesh) { //Put 1 signal to each chain so they start in the same cycle - fm.Components().ByName("d1").Inputs().ByName("i1").PutSignals(signal.New(1)) - fm.Components().ByName("d4").Inputs().ByName("i1").PutSignals(signal.New(2)) + fm.Components().ByName("d1").InputByName("i1").PutSignals(signal.New(1)) + fm.Components().ByName("d4").InputByName("i1").PutSignals(signal.New(2)) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { + assert.NoError(t, err) + result, err := fm.Components().ByName("sum").OutputByName("o1").FirstSignalPayload() assert.NoError(t, err) - result := fm.Components().ByName("sum").Outputs().ByName("o1").Signals().FirstPayload().(int) - assert.Equal(t, 16, result) + assert.Equal(t, 16, result.(int)) }, }, } diff --git a/port/collection.go b/port/collection.go index b361027..e8d17fc 100644 --- a/port/collection.go +++ b/port/collection.go @@ -1,29 +1,58 @@ package port import ( + "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) -// Collection is a port collection with useful methods -type Collection map[string]*Port +// @TODO: make type unexported +type PortMap map[string]*Port + +// Collection is a port collection +// indexed by name, hence it can not carry +// 2 ports with same name. Optimized for lookups +type Collection struct { + *common.Chainable + ports PortMap + // Labels added by default to each port in collection + defaultLabels common.LabelsCollection +} // NewCollection creates empty collection -func NewCollection() Collection { - return make(Collection) +func NewCollection() *Collection { + return &Collection{ + Chainable: common.NewChainable(), + ports: make(PortMap), + defaultLabels: common.LabelsCollection{}, + } } // ByName returns a port by its name -func (collection Collection) ByName(name string) *Port { - return collection[name] +func (collection *Collection) ByName(name string) *Port { + if collection.HasErr() { + return New("").WithErr(collection.Err()) + } + port, ok := collection.ports[name] + if !ok { + collection.SetErr(fmt.Errorf("%w, port name: %s", ErrPortNotFoundInCollection, name)) + return New("").WithErr(collection.Err()) + } + return port } // ByNames returns multiple ports by their names -func (collection Collection) ByNames(names ...string) Collection { - selectedPorts := make(Collection) +func (collection *Collection) ByNames(names ...string) *Collection { + if collection.HasErr() { + return NewCollection().WithErr(collection.Err()) + } + + //Preserve collection config + selectedPorts := NewCollection().WithDefaultLabels(collection.defaultLabels) for _, name := range names { - if p, ok := collection[name]; ok { - selectedPorts[name] = p + if p, ok := collection.ports[name]; ok { + selectedPorts.With(p) } } @@ -31,8 +60,12 @@ func (collection Collection) ByNames(names ...string) Collection { } // AnyHasSignals returns true if at least one port in collection has signals -func (collection Collection) AnyHasSignals() bool { - for _, p := range collection { +func (collection *Collection) AnyHasSignals() bool { + if collection.HasErr() { + return false + } + + for _, p := range collection.ports { if p.HasSignals() { return true } @@ -42,8 +75,12 @@ func (collection Collection) AnyHasSignals() bool { } // AllHaveSignals returns true when all ports in collection have signals -func (collection Collection) AllHaveSignals() bool { - for _, p := range collection { +func (collection *Collection) AllHaveSignals() bool { + if collection.HasErr() { + return false + } + + for _, p := range collection.ports { if !p.HasSignals() { return false } @@ -52,59 +89,151 @@ func (collection Collection) AllHaveSignals() bool { return true } -// PutSignals adds signals to every port in collection -func (collection Collection) PutSignals(signals ...*signal.Signal) { - for _, p := range collection { +// PutSignals adds buffer to every port in collection +func (collection *Collection) PutSignals(signals ...*signal.Signal) *Collection { + if collection.HasErr() { + return NewCollection().WithErr(collection.Err()) + } + + for _, p := range collection.ports { p.PutSignals(signals...) + if p.HasErr() { + return collection.WithErr(p.Err()) + } } -} -// withSignals adds signals to every port in collection and returns the collection -func (collection Collection) withSignals(signals ...*signal.Signal) Collection { - collection.PutSignals(signals...) return collection } // Clear clears all ports in collection -func (collection Collection) Clear() { - for _, p := range collection { +func (collection *Collection) Clear() *Collection { + for _, p := range collection.ports { p.Clear() + + if p.HasErr() { + return collection.WithErr(p.Err()) + } } + return collection } // Flush flushes all ports in collection -func (collection Collection) Flush() { - for _, p := range collection { - p.Flush() +func (collection *Collection) Flush() *Collection { + if collection.HasErr() { + return NewCollection().WithErr(collection.Err()) + } + + for _, p := range collection.ports { + p = p.Flush() + + if p.HasErr() { + return collection.WithErr(p.Err()) + } } + return collection } // PipeTo creates pipes from each port in collection to given destination ports -func (collection Collection) PipeTo(destPorts ...*Port) { - for _, p := range collection { - p.PipeTo(destPorts...) +func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { + for _, p := range collection.ports { + p = p.PipeTo(destPorts...) + + if p.HasErr() { + return collection.WithErr(p.Err()) + } } + + return collection } // With adds ports to collection and returns it -func (collection Collection) With(ports ...*Port) Collection { +func (collection *Collection) With(ports ...*Port) *Collection { + if collection.HasErr() { + return collection + } + for _, port := range ports { - collection[port.Name()] = port + if port.HasErr() { + return collection.WithErr(port.Err()) + } + port.AddLabels(collection.defaultLabels) + collection.ports[port.Name()] = port } return collection } // WithIndexed creates ports with names like "o1","o2","o3" and so on -func (collection Collection) WithIndexed(prefix string, startIndex int, endIndex int) Collection { - return collection.With(NewIndexedGroup(prefix, startIndex, endIndex)...) +func (collection *Collection) WithIndexed(prefix string, startIndex int, endIndex int) *Collection { + if collection.HasErr() { + return collection + } + + indexedPorts, err := NewIndexedGroup(prefix, startIndex, endIndex).Ports() + if err != nil { + collection.SetErr(err) + return NewCollection().WithErr(collection.Err()) + } + return collection.With(indexedPorts...) } -// Signals returns all signals of all ports in the group -func (collection Collection) Signals() signal.Group { +// Signals returns all signals of all ports in the collection +func (collection *Collection) Signals() *signal.Group { + if collection.HasErr() { + return signal.NewGroup().WithErr(collection.Err()) + } + group := signal.NewGroup() - for _, p := range collection { - group = append(group, p.Signals()...) + for _, p := range collection.ports { + signals, err := p.Buffer().Signals() + if err != nil { + collection.SetErr(err) + return signal.NewGroup().WithErr(collection.Err()) + } + group = group.With(signals...) } return group } + +// Ports getter +// @TODO:maybe better to hide all errors within chainable and ask user to check error ? +func (collection *Collection) Ports() (PortMap, error) { + if collection.HasErr() { + return nil, collection.Err() + } + return collection.ports, nil +} + +// PortsOrNil returns ports or nil in case of any error +func (collection *Collection) PortsOrNil() PortMap { + return collection.PortsOrDefault(nil) +} + +// PortsOrDefault returns ports or default in case of any error +func (collection *Collection) PortsOrDefault(defaultPorts PortMap) PortMap { + if collection.HasErr() { + return defaultPorts + } + + ports, err := collection.Ports() + if err != nil { + return defaultPorts + } + return ports +} + +// WithErr returns group with error +func (collection *Collection) WithErr(err error) *Collection { + collection.SetErr(err) + return collection +} + +// Len returns number of ports in collection +func (collection *Collection) Len() int { + return len(collection.ports) +} + +func (collection *Collection) WithDefaultLabels(labels common.LabelsCollection) *Collection { + collection.defaultLabels = labels + return collection +} diff --git a/port/collection_test.go b/port/collection_test.go index e33ebb3..107fc3e 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -1,23 +1,26 @@ package port import ( + "errors" + "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" "testing" ) func TestCollection_AllHaveSignal(t *testing.T) { - oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.New(123)) + oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...).PutSignals(signal.New(123)) oneEmptyPorts.ByName("p2").Clear() tests := []struct { name string - ports Collection + ports *Collection want bool }{ { name: "all empty", - ports: NewCollection().With(NewGroup("p1", "p2")...), + ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), want: false, }, { @@ -27,7 +30,7 @@ func TestCollection_AllHaveSignal(t *testing.T) { }, { name: "all set", - ports: NewCollection().With(NewGroup("out1", "out2", "out3")...).withSignals(signal.New(77)), + ports: NewCollection().With(NewGroup("out1", "out2", "out3").PortsOrNil()...).PutSignals(signal.New(77)), want: true, }, } @@ -39,12 +42,12 @@ func TestCollection_AllHaveSignal(t *testing.T) { } func TestCollection_AnyHasSignal(t *testing.T) { - oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.New(123)) + oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...).PutSignals(signal.New(123)) oneEmptyPorts.ByName("p2").Clear() tests := []struct { name string - ports Collection + ports *Collection want bool }{ { @@ -54,7 +57,7 @@ func TestCollection_AnyHasSignal(t *testing.T) { }, { name: "all empty", - ports: NewCollection().With(NewGroup("p1", "p2", "p3")...), + ports: NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...), want: false, }, } @@ -71,47 +74,55 @@ func TestCollection_ByName(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args want *Port }{ { - name: "empty port found", - collection: NewCollection().With(NewGroup("p1", "p2")...), + name: "empty port found", + collection: NewCollection().WithDefaultLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ name: "p1", }, - want: &Port{name: "p1", pipes: Group{}, signals: signal.Group{}}, + want: New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }), }, { - name: "port with signals found", - collection: NewCollection().With(NewGroup("p1", "p2")...).withSignals(signal.New(12)), + name: "port with buffer found", + collection: NewCollection().WithDefaultLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).With(NewGroup("p1", "p2").PortsOrNil()...).PutSignals(signal.New(12)), args: args{ name: "p2", }, - want: &Port{ - name: "p2", - signals: signal.NewGroup().With(signal.New(12)), - pipes: Group{}, - }, + want: New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).WithSignals(signal.New(12)), }, { name: "port not found", - collection: NewCollection().With(NewGroup("p1", "p2")...), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ name: "p3", }, - want: nil, + want: New("").WithErr(fmt.Errorf("%w, port name: %s", ErrPortNotFoundInCollection, "p3")), + }, + { + name: "with chain error", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithErr(errors.New("some error")), + args: args{ + name: "p1", + }, + want: New("").WithErr(errors.New("some error")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPort := tt.collection.ByName(tt.args.name) - if tt.want == nil { - assert.Nil(t, gotPort) - } else { - //Compare everything, but nror - } + got := tt.collection.ByName(tt.args.name) + assert.Equal(t, tt.want, got) }) } } @@ -121,82 +132,62 @@ func TestCollection_ByNames(t *testing.T) { names []string } tests := []struct { - name string - ports Collection - args args - want Collection + name string + collection *Collection + args args + want *Collection }{ { - name: "single port found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + name: "single port found", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p1"}, }, - want: Collection{ - "p1": &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, - }, + want: NewCollection().With(New("p1")), }, { - name: "multiple ports found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + name: "multiple ports found", + collection: NewCollection().With(NewGroup("p1", "p2", "p3", "p4").PortsOrNil()...), args: args{ names: []string{"p1", "p2"}, }, - want: Collection{ - "p1": &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, - "p2": &Port{ - name: "p2", - pipes: Group{}, - signals: signal.Group{}, - }, - }, + want: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), }, { - name: "single port not found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + name: "single port not found", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p7"}, }, - want: Collection{}, + want: NewCollection(), }, { - name: "some ports not found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + name: "some ports not found", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p1", "p2", "p3"}, }, - want: Collection{ - "p1": &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, - "p2": &Port{ - name: "p2", - pipes: Group{}, - signals: signal.Group{}, - }, + want: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), + }, + { + name: "with chain error", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithErr(errors.New("some error")), + args: args{ + names: []string{"p1", "p2"}, }, + want: NewCollection().WithErr(errors.New("some error")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.ports.ByNames(tt.args.names...)) + assert.Equal(t, tt.want, tt.collection.ByNames(tt.args.names...)) }) } } func TestCollection_ClearSignal(t *testing.T) { t.Run("happy path", func(t *testing.T) { - ports := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.NewGroup(1, 2, 3)...) + ports := NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...).PutSignals(signal.New(1), signal.New(2), signal.New(3)) assert.True(t, ports.AllHaveSignals()) ports.Clear() assert.False(t, ports.AnyHasSignals()) @@ -205,13 +196,13 @@ func TestCollection_ClearSignal(t *testing.T) { func TestCollection_With(t *testing.T) { type args struct { - ports []*Port + ports Ports } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "adding nothing to empty collection", @@ -219,30 +210,30 @@ func TestCollection_With(t *testing.T) { args: args{ ports: nil, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { name: "adding to empty collection", collection: NewCollection(), args: args{ - ports: NewGroup("p1", "p2"), + ports: NewGroup("p1", "p2").PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 2) - assert.Len(t, collection.ByNames("p1", "p2"), 2) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 2) + assert.Equal(t, collection.ByNames("p1", "p2").Len(), 2) }, }, { name: "adding to non-empty collection", - collection: NewCollection().With(NewGroup("p1", "p2")...), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ - ports: NewGroup("p3", "p4"), + ports: NewGroup("p3", "p4").PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 4) - assert.Len(t, collection.ByNames("p1", "p2", "p3", "p4"), 4) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 4) + assert.Equal(t, collection.ByNames("p1", "p2", "p3", "p4").Len(), 4) }, }, } @@ -259,31 +250,42 @@ func TestCollection_With(t *testing.T) { func TestCollection_Flush(t *testing.T) { tests := []struct { name string - collection Collection - assertions func(t *testing.T, collection Collection) + collection *Collection + assertions func(t *testing.T, collection *Collection) }{ { name: "empty collection", collection: NewCollection(), - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { name: "all ports in collection are flushed", collection: NewCollection().With( New("src"). - WithSignals(signal.NewGroup(1, 2, 3)...). - withPipes(New("dst1"), New("dst2")), + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }). + WithSignalGroups(signal.NewGroup(1, 2, 3)). + PipeTo(New("dst1"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), New("dst2"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), ), - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 1) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 1) assert.False(t, collection.ByName("src").HasSignals()) - for _, destPort := range collection.ByName("src").pipes { - assert.Len(t, destPort.Signals(), 3) - assert.Contains(t, destPort.Signals().AllPayloads(), 1) - assert.Contains(t, destPort.Signals().AllPayloads(), 2) - assert.Contains(t, destPort.Signals().AllPayloads(), 3) + for _, destPort := range collection.ByName("src").Pipes().PortsOrNil() { + assert.Equal(t, destPort.Buffer().Len(), 3) + allPayloads, err := destPort.AllSignalsPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 1) + assert.Contains(t, allPayloads, 2) + assert.Contains(t, allPayloads, 3) } }, }, @@ -300,35 +302,41 @@ func TestCollection_Flush(t *testing.T) { func TestCollection_PipeTo(t *testing.T) { type args struct { - destPorts []*Port + destPorts Ports } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "empty collection", collection: NewCollection(), args: args{ - destPorts: NewIndexedGroup("dest_", 1, 3), + destPorts: NewIndexedGroup("dest_", 1, 3).PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { - name: "add pipes to each port in collection", - collection: NewCollection().With(NewIndexedGroup("p", 1, 3)...), + name: "add pipes to each port in collection", + collection: NewCollection().With(NewIndexedGroup("p", 1, 3).WithPortLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PortsOrNil()...), args: args{ - destPorts: NewIndexedGroup("dest", 1, 5), + destPorts: NewIndexedGroup("dest", 1, 5). + WithPortLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }). + PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 3) - for _, p := range collection { + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 3) + for _, p := range collection.PortsOrNil() { assert.True(t, p.HasPipes()) - assert.Len(t, p.pipes, 5) + assert.Equal(t, p.Pipes().Len(), 5) } }, }, @@ -351,9 +359,9 @@ func TestCollection_WithIndexed(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "adding to empty collection", @@ -363,20 +371,20 @@ func TestCollection_WithIndexed(t *testing.T) { startIndex: 1, endIndex: 3, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 3) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 3) }, }, { name: "adding to non-empty collection", - collection: NewCollection().With(NewGroup("p1", "p2", "p3")...), + collection: NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...), args: args{ prefix: "p", startIndex: 4, endIndex: 5, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 5) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 5) }, }, } @@ -393,8 +401,8 @@ func TestCollection_WithIndexed(t *testing.T) { func TestCollection_Signals(t *testing.T) { tests := []struct { name string - collection Collection - want signal.Group + collection *Collection + want *signal.Group }{ { name: "empty collection", @@ -405,8 +413,8 @@ func TestCollection_Signals(t *testing.T) { name: "non-empty collection", collection: NewCollection(). WithIndexed("p", 1, 3). - withSignals(signal.NewGroup(1, 2, 3)...). - withSignals(signal.New("test")), + PutSignals(signal.New(1), signal.New(2), signal.New(3)). + PutSignals(signal.New("test")), want: signal.NewGroup(1, 2, 3, "test", 1, 2, 3, "test", 1, 2, 3, "test"), }, } diff --git a/port/errors.go b/port/errors.go new file mode 100644 index 0000000..b636f8c --- /dev/null +++ b/port/errors.go @@ -0,0 +1,13 @@ +package port + +import ( + "errors" +) + +var ( + ErrPortNotFoundInCollection = errors.New("port not found") + ErrInvalidRangeForIndexedGroup = errors.New("start index can not be greater than end index") + ErrNilPort = errors.New("port is nil") + ErrMissingLabel = errors.New("port is missing required label") + ErrInvalidPipeDirection = errors.New("pipe must go from output to input") +) diff --git a/port/group.go b/port/group.go index 988e01f..58080f1 100644 --- a/port/group.go +++ b/port/group.go @@ -1,42 +1,106 @@ package port -import "fmt" +import ( + "fmt" + "github.com/hovsep/fmesh/common" +) -// Group is just a slice of ports (useful to pass multiple ports as variadic argument) -type Group []*Port +type Ports []*Port + +// Group represents a list of ports +// can carry multiple ports with same name +// no lookup methods +type Group struct { + *common.Chainable + ports Ports +} // NewGroup creates multiple ports -func NewGroup(names ...string) Group { - group := make(Group, len(names)) +func NewGroup(names ...string) *Group { + newGroup := &Group{ + Chainable: common.NewChainable(), + } + ports := make(Ports, len(names)) for i, name := range names { - group[i] = New(name) + ports[i] = New(name) } - return group + return newGroup.withPorts(ports) } // NewIndexedGroup is useful to create group of ports with same prefix // NOTE: endIndex is inclusive, e.g. NewIndexedGroup("p", 0, 0) will create one port with name "p0" -func NewIndexedGroup(prefix string, startIndex int, endIndex int) Group { +func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { if startIndex > endIndex { - return nil + return NewGroup().WithErr(ErrInvalidRangeForIndexedGroup) } - group := make(Group, endIndex-startIndex+1) + ports := make(Ports, endIndex-startIndex+1) for i := startIndex; i <= endIndex; i++ { - group[i-startIndex] = New(fmt.Sprintf("%s%d", prefix, i)) + ports[i-startIndex] = New(fmt.Sprintf("%s%d", prefix, i)) } - return group + return NewGroup().withPorts(ports) } // With adds ports to group -func (group Group) With(ports ...*Port) Group { - newGroup := make(Group, len(group)+len(ports)) - copy(newGroup, group) +func (g *Group) With(ports ...*Port) *Group { + if g.HasErr() { + return g + } + + newPorts := make(Ports, len(g.ports)+len(ports)) + copy(newPorts, g.ports) for i, port := range ports { - newGroup[len(group)+i] = port + newPorts[len(g.ports)+i] = port + } + + return g.withPorts(newPorts) +} + +// withPorts sets ports +func (g *Group) withPorts(ports Ports) *Group { + g.ports = ports + return g +} + +// Ports getter +func (g *Group) Ports() (Ports, error) { + if g.HasErr() { + return nil, g.Err() } + return g.ports, nil +} + +// PortsOrNil returns ports or nil in case of any error +func (g *Group) PortsOrNil() Ports { + return g.PortsOrDefault(nil) +} + +// PortsOrDefault returns ports or default in case of any error +func (g *Group) PortsOrDefault(defaultPorts Ports) Ports { + ports, err := g.Ports() + if err != nil { + return defaultPorts + } + return ports +} - return newGroup +// WithErr returns group with error +func (g *Group) WithErr(err error) *Group { + g.SetErr(err) + return g +} + +// Len returns number of ports in group +func (g *Group) Len() int { + return len(g.ports) +} + +// WithPortLabels sets labels on each port within the group and returns it +func (g *Group) WithPortLabels(labels common.LabelsCollection) *Group { + for _, p := range g.PortsOrNil() { + p.WithLabels(labels) + } + return g } diff --git a/port/group_test.go b/port/group_test.go index 3fb8a4f..a50ee82 100644 --- a/port/group_test.go +++ b/port/group_test.go @@ -1,6 +1,7 @@ package port import ( + "github.com/hovsep/fmesh/common" "github.com/stretchr/testify/assert" "testing" ) @@ -12,23 +13,27 @@ func TestNewGroup(t *testing.T) { tests := []struct { name string args args - want Group + want *Group }{ { name: "empty group", args: args{ names: nil, }, - want: Group{}, + want: &Group{ + Chainable: common.NewChainable(), + ports: Ports{}, + }, }, { name: "non-empty group", args: args{ names: []string{"p1", "p2"}, }, - want: Group{ - New("p1"), - New("p2"), + want: &Group{ + Chainable: common.NewChainable(), + ports: Ports{New("p1"), + New("p2")}, }, }, } @@ -48,7 +53,7 @@ func TestNewIndexedGroup(t *testing.T) { tests := []struct { name string args args - want Group + want *Group }{ { name: "empty prefix is valid", @@ -75,7 +80,7 @@ func TestNewIndexedGroup(t *testing.T) { startIndex: 999, endIndex: 5, }, - want: nil, + want: NewGroup().WithErr(ErrInvalidRangeForIndexedGroup), }, } for _, tt := range tests { @@ -87,13 +92,13 @@ func TestNewIndexedGroup(t *testing.T) { func TestGroup_With(t *testing.T) { type args struct { - ports []*Port + ports Ports } tests := []struct { name string - group Group + group *Group args args - assertions func(t *testing.T, group Group) + assertions func(t *testing.T, group *Group) }{ { name: "adding nothing to empty group", @@ -101,28 +106,28 @@ func TestGroup_With(t *testing.T) { args: args{ ports: nil, }, - assertions: func(t *testing.T, group Group) { - assert.Len(t, group, 0) + assertions: func(t *testing.T, group *Group) { + assert.Zero(t, group.Len()) }, }, { name: "adding to empty group", group: NewGroup(), args: args{ - ports: NewGroup("p1", "p2", "p3"), + ports: NewGroup("p1", "p2", "p3").PortsOrNil(), }, - assertions: func(t *testing.T, group Group) { - assert.Len(t, group, 3) + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 3) }, }, { name: "adding to non-empty group", group: NewIndexedGroup("p", 1, 3), args: args{ - ports: NewGroup("p4", "p5", "p6"), + ports: NewGroup("p4", "p5", "p6").PortsOrNil(), }, - assertions: func(t *testing.T, group Group) { - assert.Len(t, group, 6) + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 6) }, }, } diff --git a/port/port.go b/port/port.go index 5b26c4e..8186d59 100644 --- a/port/port.go +++ b/port/port.go @@ -1,101 +1,250 @@ package port import ( + "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) +const ( + DirectionLabel = "fmesh:port:direction" + DirectionIn = "in" + DirectionOut = "out" +) + // Port defines a connectivity point of a component type Port struct { - name string - signals signal.Group //Signal buffer - pipes Group //Outbound pipes + common.NamedEntity + common.LabeledEntity + *common.Chainable + buffer *signal.Group + pipes *Group //Outbound pipes } // New creates a new port func New(name string) *Port { return &Port{ - name: name, - pipes: NewGroup(), - signals: signal.NewGroup(), + NamedEntity: common.NewNamedEntity(name), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + pipes: NewGroup(), + buffer: signal.NewGroup(), } + } -// Name getter -func (p *Port) Name() string { - return p.name +// Buffer getter +// @TODO: maybe we can hide this and return signals to user code +func (p *Port) Buffer() *signal.Group { + if p.HasErr() { + return signal.NewGroup().WithErr(p.Err()) + } + return p.buffer } -// Signals getter -func (p *Port) Signals() signal.Group { - return p.signals +// Pipes getter +// @TODO maybe better to return []*Port directly +func (p *Port) Pipes() *Group { + if p.HasErr() { + return NewGroup().WithErr(p.Err()) + } + return p.pipes } -// setSignals sets signals field -func (p *Port) setSignals(signals signal.Group) { - p.signals = signals +// withBuffer sets buffer field +func (p *Port) withBuffer(buffer *signal.Group) *Port { + if p.HasErr() { + return p + } + + if buffer.HasErr() { + p.SetErr(buffer.Err()) + return New("").WithErr(p.Err()) + } + p.buffer = buffer + return p } -// PutSignals adds signals +// PutSignals adds signals to buffer // @TODO: rename -func (p *Port) PutSignals(signals ...*signal.Signal) { - p.setSignals(p.Signals().With(signals...)) +func (p *Port) PutSignals(signals ...*signal.Signal) *Port { + if p.HasErr() { + return p + } + return p.withBuffer(p.Buffer().With(signals...)) } -// WithSignals adds signals and returns the port +// WithSignals puts buffer and returns the port func (p *Port) WithSignals(signals ...*signal.Signal) *Port { - p.PutSignals(signals...) + if p.HasErr() { + return p + } + + return p.PutSignals(signals...) +} + +// WithSignalGroups puts groups of buffer and returns the port +func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { + if p.HasErr() { + return p + } + for _, group := range signalGroups { + signals, err := group.Signals() + if err != nil { + p.SetErr(err) + return New("").WithErr(p.Err()) + } + p.PutSignals(signals...) + if p.HasErr() { + return New("").WithErr(p.Err()) + } + } + return p } -// Clear removes all signals from the port -func (p *Port) Clear() { - p.setSignals(signal.NewGroup()) +// Clear removes all signals from the port buffer +func (p *Port) Clear() *Port { + if p.HasErr() { + return p + } + return p.withBuffer(signal.NewGroup()) } -// Flush pushes signals to pipes and clears the port +// Flush pushes buffer to pipes and clears the port // @TODO: hide this method from user -func (p *Port) Flush() { +func (p *Port) Flush() *Port { + if p.HasErr() { + return p + } + if !p.HasSignals() || !p.HasPipes() { - return + //Log,this + //Nothing to flush + return p } - for _, outboundPort := range p.pipes { + pipes, err := p.pipes.Ports() + if err != nil { + p.SetErr(err) + return New("").WithErr(p.Err()) + } + + for _, outboundPort := range pipes { //Fan-Out - ForwardSignals(p, outboundPort) + err = ForwardSignals(p, outboundPort) + if err != nil { + p.SetErr(err) + return New("").WithErr(p.Err()) + } } - p.Clear() + return p.Clear() } -// HasSignals says whether port signals is set or not +// HasSignals says whether port buffer is set or not func (p *Port) HasSignals() bool { - return len(p.Signals()) > 0 + return len(p.AllSignalsOrNil()) > 0 } // HasPipes says whether port has outbound pipes func (p *Port) HasPipes() bool { - return len(p.pipes) > 0 + return p.Pipes().Len() > 0 } // PipeTo creates one or multiple pipes to other port(s) // @TODO: hide this method from AF -func (p *Port) PipeTo(destPorts ...*Port) { +func (p *Port) PipeTo(destPorts ...*Port) *Port { + if p.HasErr() { + return p + } + for _, destPort := range destPorts { - if destPort == nil { - continue + if err := validatePipe(p, destPort); err != nil { + p.SetErr(fmt.Errorf("pipe validation failed: %w", err)) + return New("").WithErr(p.Err()) } p.pipes = p.pipes.With(destPort) } + return p } -// withPipes adds pipes and returns the port -func (p *Port) withPipes(destPorts ...*Port) *Port { - for _, destPort := range destPorts { - p.PipeTo(destPort) +func validatePipe(srcPort *Port, dstPort *Port) error { + if srcPort == nil || dstPort == nil { + return ErrNilPort } + + srcDir, dstDir := srcPort.LabelOrDefault(DirectionLabel, ""), dstPort.LabelOrDefault(DirectionLabel, "") + + if srcDir == "" || dstDir == "" { + return ErrMissingLabel + } + + if srcDir == "in" || dstDir == "out" { + return ErrInvalidPipeDirection + } + + return nil +} + +// WithLabels sets labels and returns the port +func (p *Port) WithLabels(labels common.LabelsCollection) *Port { + if p.HasErr() { + return p + } + + p.LabeledEntity.SetLabels(labels) return p } -// ForwardSignals copies all signals from source port to destination port, without clearing the source port -func ForwardSignals(source *Port, dest *Port) { - dest.PutSignals(source.Signals()...) +// ForwardSignals copies all buffer from source port to destination port, without clearing the source port +func ForwardSignals(source *Port, dest *Port) error { + signals, err := source.AllSignals() + if err != nil { + return err + } + dest.PutSignals(signals...) + if dest.HasErr() { + return dest.Err() + } + return nil +} + +// WithErr returns port with error +func (p *Port) WithErr(err error) *Port { + p.SetErr(err) + return p +} + +// FirstSignalPayload is shortcut method +func (p *Port) FirstSignalPayload() (any, error) { + return p.Buffer().FirstPayload() +} + +// FirstSignalPayloadOrNil is shortcut method +func (p *Port) FirstSignalPayloadOrNil() any { + return p.Buffer().First().PayloadOrNil() +} + +// FirstSignalPayloadOrDefault is shortcut method +func (p *Port) FirstSignalPayloadOrDefault(defaultPayload any) any { + return p.Buffer().First().PayloadOrDefault(defaultPayload) +} + +// AllSignals is shortcut method +func (p *Port) AllSignals() (signal.Signals, error) { + return p.Buffer().Signals() +} + +// AllSignalsOrNil is shortcut method +func (p *Port) AllSignalsOrNil() signal.Signals { + return p.Buffer().SignalsOrNil() +} + +func (p *Port) AllSignalsOrDefault(defaultSignals signal.Signals) signal.Signals { + return p.Buffer().SignalsOrDefault(defaultSignals) +} + +// AllSignalsPayloads is shortcut method +func (p *Port) AllSignalsPayloads() ([]any, error) { + return p.Buffer().AllPayloads() } diff --git a/port/port_test.go b/port/port_test.go index 98b12cb..e11bd31 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -1,6 +1,8 @@ package port import ( + "errors" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" "testing" @@ -18,7 +20,7 @@ func TestPort_HasSignals(t *testing.T) { want: false, }, { - name: "port has normal signals", + name: "port has normal buffer", port: New("p").WithSignals(signal.New(123)), want: true, }, @@ -30,26 +32,31 @@ func TestPort_HasSignals(t *testing.T) { } } -func TestPort_Signals(t *testing.T) { +func TestPort_Buffer(t *testing.T) { tests := []struct { name string port *Port - want signal.Group + want *signal.Group }{ { - name: "no signals", + name: "empty buffer", port: New("noSignal"), - want: signal.Group{}, + want: signal.NewGroup(), }, { name: "with signal", port: New("p").WithSignals(signal.New(123)), want: signal.NewGroup(123), }, + { + name: "with chain error", + port: New("p").WithErr(errors.New("some error")), + want: signal.NewGroup().WithErr(errors.New("some error")), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.port.Signals()) + assert.Equal(t, tt.want, tt.port.Buffer()) }) } } @@ -63,12 +70,12 @@ func TestPort_Clear(t *testing.T) { { name: "happy path", before: New("p").WithSignals(signal.New(111)), - after: &Port{name: "p", pipes: Group{}, signals: signal.Group{}}, + after: New("p"), }, { name: "cleaning empty port", before: New("emptyPort"), - after: &Port{name: "emptyPort", pipes: Group{}, signals: signal.Group{}}, + after: New("emptyPort"), }, } for _, tt := range tests { @@ -80,124 +87,192 @@ func TestPort_Clear(t *testing.T) { } func TestPort_PipeTo(t *testing.T) { - p1, p2, p3, p4 := New("p1"), New("p2"), New("p3"), New("p4") + outputPorts := NewCollection(). + WithDefaultLabels( + common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).With( + NewIndexedGroup("out", 1, 3).PortsOrNil()..., + ) + inputPorts := NewCollection(). + WithDefaultLabels( + common.LabelsCollection{ + DirectionLabel: DirectionIn, + }).With( + NewIndexedGroup("in", 1, 3).PortsOrNil()..., + ) type args struct { - toPorts []*Port + toPorts Ports } tests := []struct { - name string - before *Port - after *Port - args args + name string + before *Port + assertions func(t *testing.T, portAfter *Port) + args args }{ { name: "happy path", - before: p1, - after: &Port{ - name: "p1", - pipes: Group{p2, p3}, - signals: signal.Group{}, + before: outputPorts.ByName("out1"), + args: args{ + toPorts: Ports{inputPorts.ByName("in2"), inputPorts.ByName("in3")}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.False(t, portAfter.HasErr()) + assert.NoError(t, portAfter.Err()) + assert.Equal(t, 2, portAfter.Pipes().Len()) + }, + }, + { + name: "port must have direction label", + before: New("out_without_dir"), + args: args{ + toPorts: Ports{inputPorts.ByName("in1")}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, + }, + { + name: "nil port is not allowed", + before: outputPorts.ByName("out3"), args: args{ - toPorts: []*Port{p2, p3}, + toPorts: Ports{inputPorts.ByName("in2"), nil}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, }, { - name: "invalid ports are ignored", - before: p4, - after: &Port{ - name: "p4", - pipes: Group{p2}, - signals: signal.Group{}, + name: "piping from input ports is not allowed", + before: inputPorts.ByName("in1"), + args: args{ + toPorts: Ports{ + inputPorts.ByName("in2"), outputPorts.ByName("out1"), + }, }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) + }, + }, + { + name: "piping to output ports is not allowed", + before: outputPorts.ByName("out1"), args: args{ - toPorts: []*Port{p2, nil}, + toPorts: Ports{outputPorts.ByName("out2")}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.before.PipeTo(tt.args.toPorts...) - assert.Equal(t, tt.after, tt.before) + portAfter := tt.before.PipeTo(tt.args.toPorts...) + if tt.assertions != nil { + tt.assertions(t, portAfter) + } }) } } func TestPort_PutSignals(t *testing.T) { type args struct { - signals []*signal.Signal + signals signal.Signals } tests := []struct { - name string - port *Port - signalsAfter signal.Group - args args + name string + port *Port + args args + assertions func(t *testing.T, portAfter *Port) }{ { - name: "single signal to empty port", - port: New("emptyPort"), - signalsAfter: signal.NewGroup(11), + name: "single signal to empty port", + port: New("emptyPort"), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11), portAfter.Buffer()) + }, args: args{ - signals: signal.NewGroup(11), + signals: signal.NewGroup(11).SignalsOrNil(), }, }, { - name: "multiple signals to empty port", - port: New("p"), - signalsAfter: signal.NewGroup(11, 12), + name: "multiple buffer to empty port", + port: New("p"), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11, 12), portAfter.Buffer()) + }, args: args{ - signals: signal.NewGroup(11, 12), + signals: signal.NewGroup(11, 12).SignalsOrNil(), }, }, { - name: "single signal to port with single signal", - port: New("p").WithSignals(signal.New(11)), - signalsAfter: signal.NewGroup(11, 12), + name: "single signal to port with single signal", + port: New("p").WithSignals(signal.New(11)), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11, 12), portAfter.Buffer()) + }, args: args{ - signals: signal.NewGroup(12), + signals: signal.NewGroup(12).SignalsOrNil(), }, }, { - name: "single signals to port with multiple signals", - port: New("p").WithSignals(signal.NewGroup(11, 12)...), - signalsAfter: signal.NewGroup(11, 12, 13), + name: "single buffer to port with multiple buffer", + port: New("p").WithSignalGroups(signal.NewGroup(11, 12)), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11, 12, 13), portAfter.Buffer()) + }, args: args{ - signals: signal.NewGroup(13), + signals: signal.NewGroup(13).SignalsOrNil(), }, }, { - name: "multiple signals to port with multiple signals", - port: New("p").WithSignals(signal.NewGroup(55, 66)...), - signalsAfter: signal.NewGroup(55, 66, 13, 14), //Notice LIFO order + name: "multiple buffer to port with multiple buffer", + port: New("p").WithSignalGroups(signal.NewGroup(55, 66)), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(55, 66, 13, 14), portAfter.Buffer()) + }, args: args{ - signals: signal.NewGroup(13, 14), + signals: signal.NewGroup(13, 14).SignalsOrNil(), }, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.port.PutSignals(tt.args.signals...) - assert.ElementsMatch(t, tt.signalsAfter, tt.port.Signals()) - }) - } -} - -func TestPort_Name(t *testing.T) { - tests := []struct { - name string - port *Port - want string - }{ { - name: "happy path", - port: New("p777"), - want: "p777", + name: "chain error propagated from buffer", + port: New("p"), + assertions: func(t *testing.T, portAfter *Port) { + assert.Zero(t, portAfter.Buffer().Len()) + assert.True(t, portAfter.Buffer().HasErr()) + }, + args: args{ + signals: signal.Signals{signal.New(111).WithErr(errors.New("some error in signal"))}, + }, + }, + { + name: "with chain error", + port: New("p").WithErr(errors.New("some error in port")), + args: args{ + signals: signal.Signals{signal.New(123)}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.True(t, portAfter.HasErr()) + assert.Zero(t, portAfter.Buffer().Len()) + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.port.Name()) + portAfter := tt.port.PutSignals(tt.args.signals...) + if tt.assertions != nil { + tt.assertions(t, portAfter) + } }) } } @@ -216,22 +291,14 @@ func TestNewPort(t *testing.T) { args: args{ name: "", }, - want: &Port{ - name: "", - pipes: Group{}, - signals: signal.Group{}, - }, + want: New(""), }, { name: "with name", args: args{ name: "p1", }, - want: &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, + want: New("p1"), }, } for _, tt := range tests { @@ -254,7 +321,11 @@ func TestPort_HasPipes(t *testing.T) { }, { name: "with pipes", - port: New("p1").withPipes(New("p2")), + port: New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PipeTo(New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), want: true, }, } @@ -272,17 +343,30 @@ func TestPort_Flush(t *testing.T) { assertions func(t *testing.T, srcPort *Port) }{ { - name: "port with signals and no pipes is not flushed", - srcPort: New("p").WithSignals(signal.NewGroup(1, 2, 3)...), + name: "port with buffer and no pipes is not flushed", + srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)), assertions: func(t *testing.T, srcPort *Port) { assert.True(t, srcPort.HasSignals()) - assert.Len(t, srcPort.Signals(), 3) + assert.Equal(t, srcPort.Buffer().Len(), 3) assert.False(t, srcPort.HasPipes()) }, }, { - name: "empty port with pipes is not flushed", - srcPort: New("p").withPipes(New("p1"), New("p2")), + name: "empty port with pipes is not flushed", + srcPort: New("p"). + WithLabels( + common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PipeTo( + New("p1"). + WithLabels( + common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), New("p2"). + WithLabels( + common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) @@ -290,47 +374,169 @@ func TestPort_Flush(t *testing.T) { }, { name: "flush to empty ports", - srcPort: New("p").WithSignals(signal.NewGroup(1, 2, 3)...). - withPipes( - New("p1"), - New("p2")), + srcPort: New("p").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).WithSignalGroups(signal.NewGroup(1, 2, 3)). + PipeTo( + New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), + New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) - for _, destPort := range srcPort.pipes { + for _, destPort := range srcPort.Pipes().PortsOrNil() { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Signals(), 3) - assert.Contains(t, destPort.Signals().AllPayloads(), 1) - assert.Contains(t, destPort.Signals().AllPayloads(), 2) - assert.Contains(t, destPort.Signals().AllPayloads(), 3) + assert.Equal(t, destPort.Buffer().Len(), 3) + allPayloads, err := destPort.AllSignalsPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 1) + assert.Contains(t, allPayloads, 2) + assert.Contains(t, allPayloads, 3) } }, }, { name: "flush to non empty ports", - srcPort: New("p").WithSignals(signal.NewGroup(1, 2, 3)...). - withPipes( - New("p1").WithSignals(signal.NewGroup(4, 5, 6)...), - New("p2").WithSignals(signal.NewGroup(7, 8, 9)...)), + srcPort: New("p").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }). + WithSignalGroups(signal.NewGroup(1, 2, 3)). + PipeTo( + New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }).WithSignalGroups(signal.NewGroup(4, 5, 6)), + New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }).WithSignalGroups(signal.NewGroup(7, 8, 9))), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) - for _, destPort := range srcPort.pipes { + for _, destPort := range srcPort.Pipes().PortsOrNil() { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Signals(), 6) - assert.Contains(t, destPort.Signals().AllPayloads(), 1) - assert.Contains(t, destPort.Signals().AllPayloads(), 2) - assert.Contains(t, destPort.Signals().AllPayloads(), 3) + assert.Equal(t, destPort.Buffer().Len(), 6) + allPayloads, err := destPort.AllSignalsPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 1) + assert.Contains(t, allPayloads, 2) + assert.Contains(t, allPayloads, 3) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.srcPort.Flush() + portAfter := tt.srcPort.Flush() + if tt.assertions != nil { + tt.assertions(t, portAfter) + } + }) + } +} + +func TestPort_WithLabels(t *testing.T) { + type args struct { + labels common.LabelsCollection + } + tests := []struct { + name string + port *Port + args args + assertions func(t *testing.T, port *Port) + }{ + { + name: "happy path", + port: New("p1"), + args: args{ + labels: common.LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, + }, + assertions: func(t *testing.T, port *Port) { + assert.Len(t, port.Labels(), 2) + assert.True(t, port.HasAllLabels("l1", "l2")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + portAfter := tt.port.WithLabels(tt.args.labels) if tt.assertions != nil { - tt.assertions(t, tt.srcPort) + tt.assertions(t, portAfter) } }) } } + +func TestPort_Pipes(t *testing.T) { + tests := []struct { + name string + port *Port + want *Group + }{ + { + name: "no pipes", + port: New("p"), + want: NewGroup(), + }, + { + name: "with pipes", + port: New("p1"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PipeTo( + New("p2"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), New("p3"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), + want: NewGroup("p2", "p3").WithPortLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), + }, + { + name: "with chain error", + port: New("p").WithErr(errors.New("some error")), + want: NewGroup().WithErr(errors.New("some error")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.port.Pipes()) + }) + } +} + +func TestPort_ShortcutGetters(t *testing.T) { + t.Run("FirstSignalPayload", func(t *testing.T) { + port := New("p").WithSignalGroups(signal.NewGroup(4, 7, 6, 5)) + payload, err := port.FirstSignalPayload() + assert.NoError(t, err) + assert.Equal(t, 4, payload) + }) + + t.Run("FirstSignalPayloadOrNil", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) + assert.Nil(t, port.FirstSignalPayloadOrNil()) + }) + + t.Run("FirstSignalPayloadOrDefault", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) + assert.Equal(t, 888, port.FirstSignalPayloadOrDefault(888)) + }) + + t.Run("AllSignalsOrNil", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) + assert.Nil(t, port.AllSignalsOrNil()) + }) + + t.Run("AllSignalsOrDefault", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) + assert.Equal(t, signal.NewGroup(999).SignalsOrNil(), port.AllSignalsOrDefault(signal.NewGroup(999).SignalsOrNil())) + }) +} diff --git a/signal/errors.go b/signal/errors.go new file mode 100644 index 0000000..7091e37 --- /dev/null +++ b/signal/errors.go @@ -0,0 +1,8 @@ +package signal + +import "errors" + +var ( + ErrNoSignalsInGroup = errors.New("group has no signals") + ErrInvalidSignal = errors.New("signal is invalid") +) diff --git a/signal/group.go b/signal/group.go index eb5d5ea..86e2f5e 100644 --- a/signal/group.go +++ b/signal/group.go @@ -1,53 +1,146 @@ package signal +import ( + "github.com/hovsep/fmesh/common" +) + +type Signals []*Signal + // Group represents a list of signals -type Group []*Signal +type Group struct { + *common.Chainable + signals Signals +} // NewGroup creates empty group -func NewGroup(payloads ...any) Group { - group := make(Group, len(payloads)) +func NewGroup(payloads ...any) *Group { + newGroup := &Group{ + Chainable: common.NewChainable(), + } + + signals := make(Signals, len(payloads)) for i, payload := range payloads { - group[i] = New(payload) + signals[i] = New(payload) } - return group + return newGroup.withSignals(signals) } // First returns the first signal in the group -func (group Group) First() *Signal { - return group[0] +func (g *Group) First() *Signal { + if g.HasErr() { + return New(nil).WithErr(g.Err()) + } + + if len(g.signals) == 0 { + g.SetErr(ErrNoSignalsInGroup) + return New(nil).WithErr(g.Err()) + } + + return g.signals[0] } // FirstPayload returns the first signal payload -func (group Group) FirstPayload() any { - return group.First().Payload() +func (g *Group) FirstPayload() (any, error) { + if g.HasErr() { + return nil, g.Err() + } + + return g.First().Payload() } // AllPayloads returns a slice with all payloads of the all signals in the group -func (group Group) AllPayloads() []any { - all := make([]any, len(group), len(group)) - for i, sig := range group { - all[i] = sig.Payload() +func (g *Group) AllPayloads() ([]any, error) { + if g.HasErr() { + return nil, g.Err() + } + + all := make([]any, len(g.signals)) + var err error + for i, sig := range g.signals { + all[i], err = sig.Payload() + if err != nil { + return nil, err + } } - return all + return all, nil } // With returns the group with added signals -func (group Group) With(signals ...*Signal) Group { - newGroup := make(Group, len(group)+len(signals)) - copy(newGroup, group) +func (g *Group) With(signals ...*Signal) *Group { + if g.HasErr() { + // Do nothing, but propagate error + return g + } + + newSignals := make(Signals, len(g.signals)+len(signals)) + copy(newSignals, g.signals) for i, sig := range signals { - newGroup[len(group)+i] = sig + if sig == nil { + g.SetErr(ErrInvalidSignal) + return NewGroup().WithErr(g.Err()) + } + + if sig.HasErr() { + g.SetErr(sig.Err()) + return NewGroup().WithErr(g.Err()) + } + + newSignals[len(g.signals)+i] = sig } - return newGroup + return g.withSignals(newSignals) } // WithPayloads returns a group with added signals created from provided payloads -func (group Group) WithPayloads(payloads ...any) Group { - newGroup := make(Group, len(group)+len(payloads)) - copy(newGroup, group) +func (g *Group) WithPayloads(payloads ...any) *Group { + if g.HasErr() { + // Do nothing, but propagate error + return g + } + + newSignals := make(Signals, len(g.signals)+len(payloads)) + copy(newSignals, g.signals) for i, p := range payloads { - newGroup[len(group)+i] = New(p) + newSignals[len(g.signals)+i] = New(p) + } + return g.withSignals(newSignals) +} + +// withSignals sets signals +func (g *Group) withSignals(signals Signals) *Group { + g.signals = signals + return g +} + +// Signals getter +func (g *Group) Signals() (Signals, error) { + if g.HasErr() { + return nil, g.Err() + } + return g.signals, nil +} + +// SignalsOrNil returns signals or nil in case of any error +func (g *Group) SignalsOrNil() Signals { + return g.SignalsOrDefault(nil) +} + +// SignalsOrDefault returns signals or default in case of any error +func (g *Group) SignalsOrDefault(defaultSignals Signals) Signals { + signals, err := g.Signals() + if err != nil { + return defaultSignals } - return newGroup + return signals +} + +// WithErr returns group with error +func (g *Group) WithErr(err error) *Group { + g.SetErr(err) + return g +} + +// Len returns number of signals in group +func (g *Group) Len() int { + return len(g.signals) } diff --git a/signal/group_test.go b/signal/group_test.go index b57984e..6d9ef5b 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -1,6 +1,7 @@ package signal import ( + "errors" "github.com/stretchr/testify/assert" "testing" ) @@ -10,76 +11,85 @@ func TestNewGroup(t *testing.T) { payloads []any } tests := []struct { - name string - args args - want Group + name string + args args + assertions func(t *testing.T, group *Group) }{ { name: "no payloads", args: args{ payloads: nil, }, - want: Group{}, + assertions: func(t *testing.T, group *Group) { + signals, err := group.Signals() + assert.NoError(t, err) + assert.Len(t, signals, 0) + assert.Zero(t, group.Len()) + }, }, { name: "with payloads", args: args{ payloads: []any{1, nil, 3}, }, - want: Group{ - &Signal{ - payload: []any{1}, - }, - &Signal{ - payload: []any{nil}, - }, - &Signal{ - payload: []any{3}, - }, + assertions: func(t *testing.T, group *Group) { + signals, err := group.Signals() + assert.NoError(t, err) + assert.Equal(t, group.Len(), 3) + assert.Contains(t, signals, New(1)) + assert.Contains(t, signals, New(nil)) + assert.Contains(t, signals, New(3)) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, NewGroup(tt.args.payloads...)) + group := NewGroup(tt.args.payloads...) + if tt.assertions != nil { + tt.assertions(t, group) + } }) } } func TestGroup_FirstPayload(t *testing.T) { tests := []struct { - name string - group Group - want any - wantPanic bool + name string + group *Group + want any + wantErrorString string }{ { - name: "empty group", - group: NewGroup(), - want: nil, - wantPanic: true, + name: "empty group", + group: NewGroup(), + want: nil, + wantErrorString: "group has no signals", + }, + { + name: "first is nil", + group: NewGroup(nil, 123), + want: nil, }, { - name: "first is nil", - group: NewGroup(nil, 123), - want: nil, - wantPanic: false, + name: "first is not nil", + group: NewGroup([]string{"1", "2"}, 123), + want: []string{"1", "2"}, }, { - name: "first is not nil", - group: NewGroup([]string{"1", "2"}, 123), - want: []string{"1", "2"}, - wantPanic: false, + name: "with error in chain", + group: NewGroup(3, 4, 5).WithErr(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.wantPanic { - assert.Panics(t, func() { - tt.group.FirstPayload() - }) + got, err := tt.group.FirstPayload() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) } else { - assert.Equal(t, tt.want, tt.group.FirstPayload()) + assert.Equal(t, tt.want, got) } }) } @@ -87,9 +97,10 @@ func TestGroup_FirstPayload(t *testing.T) { func TestGroup_AllPayloads(t *testing.T) { tests := []struct { - name string - group Group - want []any + name string + group *Group + want []any + wantErrorString string }{ { name: "empty group", @@ -101,23 +112,41 @@ func TestGroup_AllPayloads(t *testing.T) { group: NewGroup(1, nil, 3, []int{4, 5, 6}, map[byte]byte{7: 8}), want: []any{1, nil, 3, []int{4, 5, 6}, map[byte]byte{7: 8}}, }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", + }, + { + name: "with error in signal", + group: NewGroup().withSignals(Signals{New(33).WithErr(errors.New("some error in signal"))}), + want: nil, + wantErrorString: "some error in signal", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.group.AllPayloads()) + got, err := tt.group.AllPayloads() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } }) } } func TestGroup_With(t *testing.T) { type args struct { - signals []*Signal + signals Signals } tests := []struct { name string - group Group + group *Group args args - want Group + want *Group }{ { name: "no addition to empty group", @@ -139,7 +168,7 @@ func TestGroup_With(t *testing.T) { name: "addition to empty group", group: NewGroup(), args: args{ - signals: NewGroup(3, 4, 5), + signals: NewGroup(3, 4, 5).SignalsOrNil(), }, want: NewGroup(3, 4, 5), }, @@ -147,14 +176,43 @@ func TestGroup_With(t *testing.T) { name: "addition to group", group: NewGroup(1, 2, 3), args: args{ - signals: NewGroup(4, 5, 6), + signals: NewGroup(4, 5, 6).SignalsOrNil(), }, want: NewGroup(1, 2, 3, 4, 5, 6), }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3). + With(New("valid before invalid")). + With(nil). + WithPayloads(4, 5, 6), + args: args{ + signals: NewGroup(7, nil, 9).SignalsOrNil(), + }, + want: NewGroup().WithErr(errors.New("signal is invalid")), + }, + { + name: "with error in signal", + group: NewGroup(1, 2, 3).With(New(44).WithErr(errors.New("some error in signal"))), + args: args{ + signals: Signals{New(456)}, + }, + want: NewGroup(1, 2, 3). + With(New(44). + WithErr(errors.New("some error in signal"))). + WithErr(errors.New("some error in signal")), // error propagated from signal to group + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.group.With(tt.args.signals...)) + got := tt.group.With(tt.args.signals...) + if tt.want.HasErr() { + assert.Error(t, got.Err()) + assert.EqualError(t, got.Err(), tt.want.Err().Error()) + } else { + assert.NoError(t, got.Err()) + } + assert.Equal(t, tt.want, got) }) } } @@ -165,9 +223,9 @@ func TestGroup_WithPayloads(t *testing.T) { } tests := []struct { name string - group Group + group *Group args args - want Group + want *Group }{ { name: "no addition to empty group", @@ -208,3 +266,128 @@ func TestGroup_WithPayloads(t *testing.T) { }) } } + +func TestGroup_First(t *testing.T) { + tests := []struct { + name string + group *Group + want *Signal + }{ + { + name: "empty group", + group: NewGroup(), + want: New(nil).WithErr(errors.New("group has no signals")), + }, + { + name: "happy path", + group: NewGroup(3, 5, 7), + want: New(3), + }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), + want: New(nil).WithErr(errors.New("some error in chain")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.group.First() + if tt.want.HasErr() { + assert.True(t, got.HasErr()) + assert.Error(t, got.Err()) + assert.EqualError(t, got.Err(), tt.want.Err().Error()) + } else { + assert.Equal(t, tt.want, tt.group.First()) + } + }) + } +} + +func TestGroup_Signals(t *testing.T) { + tests := []struct { + name string + group *Group + want Signals + wantErrorString string + }{ + { + name: "empty group", + group: NewGroup(), + want: Signals{}, + wantErrorString: "", + }, + { + name: "with signals", + group: NewGroup(1, nil, 3), + want: Signals{New(1), New(nil), New(3)}, + wantErrorString: "", + }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.group.Signals() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestGroup_SignalsOrDefault(t *testing.T) { + type args struct { + defaultSignals Signals + } + tests := []struct { + name string + group *Group + args args + want Signals + }{ + { + name: "empty group", + group: NewGroup(), + args: args{ + defaultSignals: nil, + }, + want: Signals{}, // Empty group has empty slice of signals + }, + { + name: "with signals", + group: NewGroup(1, 2, 3), + args: args{ + defaultSignals: Signals{New(4), New(5)}, //Default must be ignored + }, + want: Signals{New(1), New(2), New(3)}, + }, + { + name: "with error in chain and nil default", + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), + args: args{ + defaultSignals: nil, + }, + want: nil, + }, + { + name: "with error in chain and default", + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), + args: args{ + defaultSignals: Signals{New(4), New(5)}, + }, + want: Signals{New(4), New(5)}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.group.SignalsOrDefault(tt.args.defaultSignals)) + }) + } +} diff --git a/signal/signal.go b/signal/signal.go index 7c9bcc1..f33b475 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -1,16 +1,45 @@ package signal +import "github.com/hovsep/fmesh/common" + // Signal is a wrapper around the data flowing between components type Signal struct { + *common.Chainable payload []any //Slice is used in order to support nil payload } // New creates a new signal from the given payloads func New(payload any) *Signal { - return &Signal{payload: []any{payload}} + return &Signal{ + Chainable: common.NewChainable(), + payload: []any{payload}, + } } // Payload getter -func (s *Signal) Payload() any { - return s.payload[0] +func (s *Signal) Payload() (any, error) { + if s.HasErr() { + return nil, s.Err() + } + return s.payload[0], nil +} + +// PayloadOrNil returns payload or nil in case of error +func (s *Signal) PayloadOrNil() any { + return s.PayloadOrDefault(nil) +} + +// PayloadOrDefault returns payload or provided default value in case of error +func (s *Signal) PayloadOrDefault(defaultPayload any) any { + payload, err := s.Payload() + if err != nil { + return defaultPayload + } + return payload +} + +// WithErr returns signal with error +func (s *Signal) WithErr(err error) *Signal { + s.SetErr(err) + return s } diff --git a/signal/signal_test.go b/signal/signal_test.go index a520683..0bccc01 100644 --- a/signal/signal_test.go +++ b/signal/signal_test.go @@ -1,6 +1,8 @@ package signal import ( + "errors" + "github.com/hovsep/fmesh/common" "github.com/stretchr/testify/assert" "testing" ) @@ -20,7 +22,8 @@ func TestNew(t *testing.T) { payload: nil, }, want: &Signal{ - payload: []any{nil}, + payload: []any{nil}, + Chainable: &common.Chainable{}, }, }, { @@ -28,8 +31,9 @@ func TestNew(t *testing.T) { args: args{ payload: []any{123, "hello", []int{1, 2, 3}, map[string]int{"key": 42}, []byte{}, nil}, }, - want: &Signal{payload: []any{ - []any{123, "hello", []int{1, 2, 3}, map[string]int{"key": 42}, []byte{}, nil}}, + want: &Signal{ + payload: []any{[]any{123, "hello", []int{1, 2, 3}, map[string]int{"key": 42}, []byte{}, nil}}, + Chainable: &common.Chainable{}, }, }, } @@ -42,9 +46,10 @@ func TestNew(t *testing.T) { func TestSignal_Payload(t *testing.T) { tests := []struct { - name string - signal *Signal - want any + name string + signal *Signal + want any + wantErrorString string }{ { name: "nil payload is valid", @@ -56,10 +61,47 @@ func TestSignal_Payload(t *testing.T) { signal: New(123), want: 123, }, + { + name: "with error in chain", + signal: New(123).WithErr(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.signal.Payload() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } + + }) + } +} + +func TestSignal_PayloadOrNil(t *testing.T) { + tests := []struct { + name string + signal *Signal + want any + }{ + { + name: "payload returned", + signal: New(123), + want: 123, + }, + { + name: "nil returned", + signal: New(123).WithErr(errors.New("some error in chain")), + want: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.signal.Payload()) + assert.Equal(t, tt.want, tt.signal.PayloadOrNil()) }) } }