Skip to content

Commit 4e603e3

Browse files
feat(operation): introduce operation component
Introducing the first building block for the operations API, this commit introduces the Operation component. Currently, it does not cover Report, retry policy , those will be in future PRs JIRA: https://smartcontract-it.atlassian.net/browse/CLD-16
1 parent 4609f1a commit 4e603e3

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

deployment/operations/operation.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package operations
2+
3+
import (
4+
"context"
5+
6+
"github.com/Masterminds/semver/v3"
7+
"github.com/google/uuid"
8+
9+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
10+
)
11+
12+
// OperationEnv is the env passed to the OperationHandler.
13+
// It contains the logger and the context.
14+
// Use NewOperationEnv to create a new OperationEnv.
15+
type OperationEnv struct {
16+
logger logger.Logger
17+
getContext func() context.Context
18+
}
19+
20+
// NewOperationEnv creates and returns a new OperationEnv.
21+
func NewOperationEnv(getContext func() context.Context, logger logger.Logger) OperationEnv {
22+
return OperationEnv{
23+
logger: logger,
24+
getContext: getContext,
25+
}
26+
}
27+
28+
// OperationHandler is the function signature of an operation handler.
29+
type OperationHandler[I, O, D any] func(e OperationEnv, deps D, input I) (output O, err error)
30+
31+
// OperationDefinition is the metadata of an operation.
32+
type OperationDefinition struct {
33+
ID string
34+
Version semver.Version
35+
Description string
36+
}
37+
38+
// Operation is the low level building blocks of the Operations API.
39+
// Developers define their own operation with custom input and output types.
40+
// Each operation should only perform max 1 side effect (e.g. send a transaction, post a job spec...)
41+
// Use NewOperation to create a new operation.
42+
type Operation[Input, Output, Deps any] struct {
43+
def OperationDefinition
44+
handler OperationHandler[Input, Output, Deps]
45+
}
46+
47+
// NewOperation creates a new operation.
48+
// Note: The handler should only perform maximum 1 side effect.
49+
func NewOperation[I, O, D any](
50+
version semver.Version, description string, handler OperationHandler[I, O, D],
51+
) *Operation[I, O, D] {
52+
return &Operation[I, O, D]{
53+
def: OperationDefinition{
54+
ID: uuid.New().String(),
55+
Version: version,
56+
Description: description,
57+
},
58+
handler: handler,
59+
}
60+
}
61+
62+
// Execute runs the operation by calling the OperationHandler.
63+
func (o *Operation[I, O, D]) Execute(e OperationEnv, deps D, input I) (output O, err error) {
64+
e.logger.Infow("Executing operation",
65+
"id", o.def.ID, "version", o.def.Version, "description", o.def.Description)
66+
return o.handler(e, deps, input)
67+
}
68+
69+
// ID returns the operation ID.
70+
func (o *Operation[I, O, D]) ID() string {
71+
return o.def.ID
72+
}
73+
74+
// Version returns the operation semver version in string.
75+
func (o *Operation[I, O, D]) Version() string {
76+
return o.def.Version.String()
77+
}
78+
79+
// Description returns the operation description.
80+
func (o *Operation[I, O, D]) Description() string {
81+
return o.def.Description
82+
}
83+
84+
// EmptyInput is a placeholder for operations that do not require input.
85+
type EmptyInput struct{}
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package operations
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/Masterminds/semver/v3"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"go.uber.org/zap/zapcore"
11+
12+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
13+
)
14+
15+
type OpDeps struct{}
16+
17+
type OpInput struct {
18+
A int
19+
B int
20+
}
21+
22+
func Test_NewOperation(t *testing.T) {
23+
version := semver.MustParse("1.0.0")
24+
description := "test operation"
25+
handler := func(e OperationEnv, deps OpDeps, input OpInput) (output int, err error) {
26+
return input.A + input.B, nil
27+
}
28+
29+
op := NewOperation(*version, description, handler)
30+
31+
assert.Equal(t, version.String(), op.Version())
32+
assert.NotEmpty(t, op.ID())
33+
assert.Equal(t, description, op.def.Description)
34+
res, err := op.handler(OperationEnv{}, OpDeps{}, OpInput{1, 2})
35+
require.NoError(t, err)
36+
assert.Equal(t, 3, res)
37+
}
38+
39+
func Test_Operation_Execute(t *testing.T) {
40+
version := semver.MustParse("1.0.0")
41+
description := "test operation"
42+
log, observedLog := logger.TestObserved(t, zapcore.InfoLevel)
43+
44+
// simulate an addition operation
45+
handler := func(e OperationEnv, deps OpDeps, input OpInput) (output int, err error) {
46+
return input.A + input.B, nil
47+
}
48+
49+
op := NewOperation(*version, description, handler)
50+
e := NewOperationEnv(context.Background, log)
51+
input := OpInput{
52+
A: 1,
53+
B: 2,
54+
}
55+
56+
output, err := op.Execute(e, OpDeps{}, input)
57+
58+
require.NoError(t, err)
59+
assert.Equal(t, 3, output)
60+
61+
require.Equal(t, 1, observedLog.Len())
62+
entry := observedLog.All()[0]
63+
assert.Equal(t, "Executing operation", entry.Message)
64+
assert.NotEmpty(t, entry.ContextMap()["id"])
65+
assert.Equal(t, version.String(), entry.ContextMap()["version"])
66+
assert.Equal(t, description, entry.ContextMap()["description"])
67+
}

0 commit comments

Comments
 (0)