diff --git a/README.md b/README.md
index ac37dfd..217ccf3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
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:
- F-Mesh consists of multiple Components - the main building blocks
- Components have unlimited number of input and output Ports
-- The main job of each component is to read inputs and provide outputs
- Any output port can be connected to any input port via Pipes
-- The component behaviour is defined by its Activation function
-- The framework checks when components are ready to be activated and calls their activation functions concurrently
-- One such iteration is called Activation cycle
-- On each activation cycle the framework does same things: activates all the components ready for activation, flushes the data through pipes and disposes input Signals (the data chunks flowing between components)
-- Ports and pipes are type agnostic, any data can be transferred or aggregated on any port
-- The framework works in discrete time, not it wall time. The quant of time is 1 activation cycle, which gives you "logical parallelism" out of the box
-- F-Mesh is suitable for logical wireframing, simulation, functional-style computations and implementing simple concurrency patterns without using the concurrency primitives like channels or any sort of locks
+- Ports and pipes are type agnostic, any data can be transferred to any port
+- The framework works in discrete time, not it wall time. The quant of time is 1 activation cycle, which gives you "logical parallelism" out of the box (activation function is running in "frozen time")
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 }}
+
+ Description: | {{ .meshDescription }} |
+
+ {{ end }}
+
+ {{ if .cycleNumber }}
+
+ Cycle: | {{ .cycleNumber }} |
+
+ {{ end }}
+
+ {{ if .stats }}
+ {{ range .stats }}
+
+ {{ .Name }}: | {{ .Value }} |
+
+ {{ end }}
+ {{ end }}
+
+ `
+)
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())
})
}
}