diff --git a/clients/http/const_test.go b/clients/http/const_test.go index e51174bc..899f91e7 100644 --- a/clients/http/const_test.go +++ b/clients/http/const_test.go @@ -18,4 +18,10 @@ const ( TestCategory = "health-check" TestLabel = "rest" ExampleUUID = "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" + + TestScheduleJobName = "TestScheduleJobName" + TestInterval = "10m" + TestCrontab = "0 0 1 1 *" + TestTopic = "TestTopic" + TestAddress = "TestAddress" ) diff --git a/clients/http/scheduleactionrecord.go b/clients/http/scheduleactionrecord.go new file mode 100644 index 00000000..8f288f61 --- /dev/null +++ b/clients/http/scheduleactionrecord.go @@ -0,0 +1,105 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "net/url" + "path" + "strconv" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/http/utils" + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +type ScheduleActionRecordClient struct { + baseUrl string + authInjector interfaces.AuthenticationInjector + enableNameFieldEscape bool +} + +// NewScheduleActionRecordClient creates an instance of ScheduleActionRecordClient +func NewScheduleActionRecordClient(baseUrl string, authInjector interfaces.AuthenticationInjector, enableNameFieldEscape bool) interfaces.ScheduleActionRecordClient { + return &ScheduleActionRecordClient{ + baseUrl: baseUrl, + authInjector: authInjector, + enableNameFieldEscape: enableNameFieldEscape, + } +} + +// AllScheduleActionRecords query schedule action records with start, end, offset, and limit +func (client *ScheduleActionRecordClient) AllScheduleActionRecords(ctx context.Context, start, end int64, offset, limit int) (res responses.MultiScheduleActionRecordsResponse, err errors.EdgeX) { + requestParams := url.Values{} + requestParams.Set(common.Start, strconv.FormatInt(start, 10)) + requestParams.Set(common.End, strconv.FormatInt(end, 10)) + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, client.baseUrl, common.ApiAllScheduleActionRecordRoute, requestParams, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// LatestScheduleActionRecords query the latest schedule action records of each schedule job with offset, and limit +func (client *ScheduleActionRecordClient) LatestScheduleActionRecords(ctx context.Context, offset, limit int) (res responses.MultiScheduleActionRecordsResponse, err errors.EdgeX) { + requestParams := url.Values{} + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, client.baseUrl, common.ApiLatestScheduleActionRecordRoute, requestParams, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// ScheduleActionRecordsByStatus queries schedule action records with status, start, end, offset, and limit +func (client *ScheduleActionRecordClient) ScheduleActionRecordsByStatus(ctx context.Context, status string, start, end int64, offset, limit int) (res responses.MultiScheduleActionRecordsResponse, err errors.EdgeX) { + requestPath := path.Join(common.ApiScheduleActionRecordRoute, common.Status, status) + requestParams := url.Values{} + requestParams.Set(common.Start, strconv.FormatInt(start, 10)) + requestParams.Set(common.End, strconv.FormatInt(end, 10)) + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, client.baseUrl, requestPath, requestParams, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// ScheduleActionRecordsByJobName queries schedule action records with jobName, start, end, offset, and limit +func (client *ScheduleActionRecordClient) ScheduleActionRecordsByJobName(ctx context.Context, jobName string, start, end int64, offset, limit int) (res responses.MultiScheduleActionRecordsResponse, err errors.EdgeX) { + requestPath := path.Join(common.ApiScheduleActionRecordRoute, common.Job, common.Name, jobName) + requestParams := url.Values{} + requestParams.Set(common.Start, strconv.FormatInt(start, 10)) + requestParams.Set(common.End, strconv.FormatInt(end, 10)) + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, client.baseUrl, requestPath, requestParams, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// ScheduleActionRecordsByJobNameAndStatus queries schedule action records with jobName, status, start, end, offset, and limit +func (client *ScheduleActionRecordClient) ScheduleActionRecordsByJobNameAndStatus(ctx context.Context, jobName, status string, start, end int64, offset, limit int) (res responses.MultiScheduleActionRecordsResponse, err errors.EdgeX) { + requestPath := path.Join(common.ApiScheduleActionRecordRoute, common.Job, common.Name, jobName, common.Status, status) + requestParams := url.Values{} + requestParams.Set(common.Start, strconv.FormatInt(start, 10)) + requestParams.Set(common.End, strconv.FormatInt(end, 10)) + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, client.baseUrl, requestPath, requestParams, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} diff --git a/clients/http/scheduleactionrecord_test.go b/clients/http/scheduleactionrecord_test.go new file mode 100644 index 00000000..2a57155a --- /dev/null +++ b/clients/http/scheduleactionrecord_test.go @@ -0,0 +1,71 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "net/http" + "path" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +func TestScheduleActionRecordClient_AllScheduleActionRecords(t *testing.T) { + ts := newTestServer(http.MethodGet, common.ApiAllScheduleActionRecordRoute, responses.MultiScheduleActionRecordsResponse{}) + defer ts.Close() + client := NewScheduleActionRecordClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.AllScheduleActionRecords(context.Background(), 0, 0, 0, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiScheduleActionRecordsResponse{}, res) +} + +func TestScheduleActionRecordClient_LatestScheduleActionRecords(t *testing.T) { + ts := newTestServer(http.MethodGet, common.ApiLatestScheduleActionRecordRoute, responses.MultiScheduleActionRecordsResponse{}) + defer ts.Close() + client := NewScheduleActionRecordClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.LatestScheduleActionRecords(context.Background(), 0, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiScheduleActionRecordsResponse{}, res) +} + +func TestScheduleActionRecordClient_ScheduleActionRecordsByStatus(t *testing.T) { + status := models.Succeeded + urlPath := path.Join(common.ApiScheduleActionRecordRoute, common.Status, status) + ts := newTestServer(http.MethodGet, urlPath, responses.MultiScheduleActionRecordsResponse{}) + defer ts.Close() + client := NewScheduleActionRecordClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.ScheduleActionRecordsByStatus(context.Background(), status, 0, 0, 0, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiScheduleActionRecordsResponse{}, res) +} + +func TestScheduleActionRecordClient_ScheduleActionRecordsByJobName(t *testing.T) { + jobName := TestScheduleJobName + urlPath := path.Join(common.ApiScheduleActionRecordRoute, common.Job, common.Name, jobName) + ts := newTestServer(http.MethodGet, urlPath, responses.MultiScheduleActionRecordsResponse{}) + defer ts.Close() + client := NewScheduleActionRecordClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.ScheduleActionRecordsByJobName(context.Background(), jobName, 0, 0, 0, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiScheduleActionRecordsResponse{}, res) +} + +func TestScheduleActionRecordClient_ScheduleActionRecordsByJobNameAndStatus(t *testing.T) { + jobName := TestScheduleJobName + status := models.Succeeded + urlPath := path.Join(common.ApiScheduleActionRecordRoute, common.Job, common.Name, jobName, common.Status, status) + ts := newTestServer(http.MethodGet, urlPath, responses.MultiScheduleActionRecordsResponse{}) + defer ts.Close() + client := NewScheduleActionRecordClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.ScheduleActionRecordsByJobNameAndStatus(context.Background(), jobName, status, 0, 0, 0, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiScheduleActionRecordsResponse{}, res) +} diff --git a/clients/http/schedulejob.go b/clients/http/schedulejob.go new file mode 100644 index 00000000..e948a4ff --- /dev/null +++ b/clients/http/schedulejob.go @@ -0,0 +1,104 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "net/url" + "strconv" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/http/utils" + "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +type ScheduleJobClient struct { + baseUrl string + authInjector interfaces.AuthenticationInjector + enableNameFieldEscape bool +} + +// NewScheduleJobClient creates an instance of ScheduleJobClient +func NewScheduleJobClient(baseUrl string, authInjector interfaces.AuthenticationInjector, enableNameFieldEscape bool) interfaces.ScheduleJobClient { + return &ScheduleJobClient{ + baseUrl: baseUrl, + authInjector: authInjector, + enableNameFieldEscape: enableNameFieldEscape, + } +} + +// Add adds new schedule jobs +func (client ScheduleJobClient) Add(ctx context.Context, reqs []requests.AddScheduleJobRequest) ( + res []dtoCommon.BaseWithIdResponse, err errors.EdgeX) { + err = utils.PostRequestWithRawData(ctx, &res, client.baseUrl, common.ApiScheduleJobRoute, nil, reqs, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// Update updates schedule jobs +func (client ScheduleJobClient) Update(ctx context.Context, reqs []requests.UpdateScheduleJobRequest) ( + res []dtoCommon.BaseResponse, err errors.EdgeX) { + err = utils.PatchRequest(ctx, &res, client.baseUrl, common.ApiScheduleJobRoute, nil, reqs, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// AllScheduleJobs queries the schedule jobs with offset, limit +func (client ScheduleJobClient) AllScheduleJobs(ctx context.Context, offset int, limit int) ( + res responses.MultiScheduleJobsResponse, err errors.EdgeX) { + requestParams := url.Values{} + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, client.baseUrl, common.ApiAllScheduleJobRoute, requestParams, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// ScheduleJobByName queries the schedule job by name +func (client ScheduleJobClient) ScheduleJobByName(ctx context.Context, name string) ( + res responses.ScheduleJobResponse, err errors.EdgeX) { + path := common.NewPathBuilder().EnableNameFieldEscape(client.enableNameFieldEscape). + SetPath(common.ApiScheduleJobRoute).SetPath(common.Name).SetNameFieldPath(name).BuildPath() + err = utils.GetRequest(ctx, &res, client.baseUrl, path, nil, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// DeleteScheduleJobByName deletes the schedule job by name +func (client ScheduleJobClient) DeleteScheduleJobByName(ctx context.Context, name string) ( + res dtoCommon.BaseResponse, err errors.EdgeX) { + path := common.NewPathBuilder().EnableNameFieldEscape(client.enableNameFieldEscape). + SetPath(common.ApiScheduleJobRoute).SetPath(common.Name).SetNameFieldPath(name).BuildPath() + err = utils.DeleteRequest(ctx, &res, client.baseUrl, path, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + +// TriggerScheduleJobByName triggers the schedule job by name +func (client ScheduleJobClient) TriggerScheduleJobByName(ctx context.Context, name string) ( + res dtoCommon.BaseResponse, err errors.EdgeX) { + path := common.NewPathBuilder().EnableNameFieldEscape(client.enableNameFieldEscape). + SetPath(common.ApiTriggerScheduleJobRoute).SetPath(common.Name).SetNameFieldPath(name).BuildPath() + err = utils.PostRequestWithRawData(ctx, &res, client.baseUrl, path, nil, nil, client.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} diff --git a/clients/http/schedulejob_test.go b/clients/http/schedulejob_test.go new file mode 100644 index 00000000..ca4edef4 --- /dev/null +++ b/clients/http/schedulejob_test.go @@ -0,0 +1,131 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package http + +import ( + "context" + "net/http" + "path" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +func addScheduleJobRequest() requests.AddScheduleJobRequest { + return requests.NewAddScheduleJobRequest( + dtos.ScheduleJob{ + Name: TestScheduleJobName, + Definition: dtos.ScheduleDef{ + Type: common.DefInterval, + IntervalScheduleDef: dtos.IntervalScheduleDef{ + Interval: TestInterval, + }, + }, + Actions: []dtos.ScheduleAction{ + { + Type: common.ActionEdgeXMessageBus, + ContentType: common.ContentTypeJSON, + Payload: nil, + EdgeXMessageBusAction: dtos.EdgeXMessageBusAction{ + Topic: TestTopic, + }, + }, + { + Type: common.ActionREST, + ContentType: common.ContentTypeJSON, + Payload: nil, + RESTAction: dtos.RESTAction{ + Address: TestAddress, + }, + }, + }, + Labels: []string{TestLabel}, + AdminState: models.Unlocked, + }, + ) +} + +func updateScheduleJobRequest() requests.UpdateScheduleJobRequest { + name := TestSubscriptionName + return requests.NewUpdateScheduleJobRequest( + dtos.UpdateScheduleJob{ + Name: &name, + Definition: &dtos.ScheduleDef{ + Type: common.DefCron, + CronScheduleDef: dtos.CronScheduleDef{ + Crontab: TestCrontab, + }, + }, + }, + ) +} + +func TestScheduleJobClient_Add(t *testing.T) { + ts := newTestServer(http.MethodPost, common.ApiScheduleJobRoute, []dtoCommon.BaseWithIdResponse{}) + defer ts.Close() + client := NewScheduleJobClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.Add(context.Background(), []requests.AddScheduleJobRequest{addScheduleJobRequest()}) + require.NoError(t, err) + require.IsType(t, []dtoCommon.BaseWithIdResponse{}, res) +} + +func TestScheduleJobClient_Update(t *testing.T) { + ts := newTestServer(http.MethodPatch, common.ApiScheduleJobRoute, []dtoCommon.BaseResponse{}) + defer ts.Close() + client := NewScheduleJobClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.Update(context.Background(), []requests.UpdateScheduleJobRequest{updateScheduleJobRequest()}) + require.NoError(t, err) + require.IsType(t, []dtoCommon.BaseResponse{}, res) +} + +func TestScheduleJobClient_AllScheduleJobs(t *testing.T) { + ts := newTestServer(http.MethodGet, common.ApiAllScheduleJobRoute, responses.MultiScheduleJobsResponse{}) + defer ts.Close() + client := NewScheduleJobClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.AllScheduleJobs(context.Background(), 0, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiScheduleJobsResponse{}, res) +} + +func TestScheduleJobClient_ScheduleJobByName(t *testing.T) { + scheduleJobName := TestScheduleJobName + path := path.Join(common.ApiScheduleJobRoute, common.Name, scheduleJobName) + ts := newTestServer(http.MethodGet, path, responses.ScheduleJobResponse{}) + defer ts.Close() + client := NewScheduleJobClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.ScheduleJobByName(context.Background(), scheduleJobName) + require.NoError(t, err) + require.IsType(t, responses.ScheduleJobResponse{}, res) +} + +func TestScheduleJobClient_DeleteScheduleJobByName(t *testing.T) { + scheduleJobName := TestScheduleJobName + path := path.Join(common.ApiScheduleJobRoute, common.Name, scheduleJobName) + ts := newTestServer(http.MethodDelete, path, dtoCommon.BaseResponse{}) + defer ts.Close() + client := NewScheduleJobClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.DeleteScheduleJobByName(context.Background(), scheduleJobName) + require.NoError(t, err) + require.IsType(t, dtoCommon.BaseResponse{}, res) +} + +func TestScheduleJobClient_TriggerScheduleJobByName(t *testing.T) { + scheduleJobName := TestScheduleJobName + path := path.Join(common.ApiTriggerScheduleJobRoute, common.Name, scheduleJobName) + ts := newTestServer(http.MethodPost, path, dtoCommon.BaseResponse{}) + defer ts.Close() + client := NewScheduleJobClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.TriggerScheduleJobByName(context.Background(), scheduleJobName) + require.NoError(t, err) + require.IsType(t, dtoCommon.BaseResponse{}, res) +} diff --git a/clients/interfaces/mocks/ScheduleActionRecordClient.go b/clients/interfaces/mocks/ScheduleActionRecordClient.go new file mode 100644 index 00000000..b87e3ece --- /dev/null +++ b/clients/interfaces/mocks/ScheduleActionRecordClient.go @@ -0,0 +1,182 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + errors "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" + + mock "github.com/stretchr/testify/mock" + + responses "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" +) + +// ScheduleActionRecordClient is an autogenerated mock type for the ScheduleActionRecordClient type +type ScheduleActionRecordClient struct { + mock.Mock +} + +// AllScheduleActionRecords provides a mock function with given fields: ctx, start, end, offset, limit +func (_m *ScheduleActionRecordClient) AllScheduleActionRecords(ctx context.Context, start int64, end int64, offset int, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) { + ret := _m.Called(ctx, start, end, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for AllScheduleActionRecords") + } + + var r0 responses.MultiScheduleActionRecordsResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int, int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX)); ok { + return rf(ctx, start, end, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int, int) responses.MultiScheduleActionRecordsResponse); ok { + r0 = rf(ctx, start, end, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiScheduleActionRecordsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, int64, int, int) errors.EdgeX); ok { + r1 = rf(ctx, start, end, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// LatestScheduleActionRecords provides a mock function with given fields: ctx, offset, limit +func (_m *ScheduleActionRecordClient) LatestScheduleActionRecords(ctx context.Context, offset int, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) { + ret := _m.Called(ctx, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for LatestScheduleActionRecords") + } + + var r0 responses.MultiScheduleActionRecordsResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, int, int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX)); ok { + return rf(ctx, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int, int) responses.MultiScheduleActionRecordsResponse); ok { + r0 = rf(ctx, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiScheduleActionRecordsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, int, int) errors.EdgeX); ok { + r1 = rf(ctx, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// ScheduleActionRecordsByJobName provides a mock function with given fields: ctx, jobName, start, end, offset, limit +func (_m *ScheduleActionRecordClient) ScheduleActionRecordsByJobName(ctx context.Context, jobName string, start int64, end int64, offset int, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) { + ret := _m.Called(ctx, jobName, start, end, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for ScheduleActionRecordsByJobName") + } + + var r0 responses.MultiScheduleActionRecordsResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string, int64, int64, int, int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX)); ok { + return rf(ctx, jobName, start, end, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64, int64, int, int) responses.MultiScheduleActionRecordsResponse); ok { + r0 = rf(ctx, jobName, start, end, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiScheduleActionRecordsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64, int64, int, int) errors.EdgeX); ok { + r1 = rf(ctx, jobName, start, end, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// ScheduleActionRecordsByJobNameAndStatus provides a mock function with given fields: ctx, jobName, status, start, end, offset, limit +func (_m *ScheduleActionRecordClient) ScheduleActionRecordsByJobNameAndStatus(ctx context.Context, jobName string, status string, start int64, end int64, offset int, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) { + ret := _m.Called(ctx, jobName, status, start, end, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for ScheduleActionRecordsByJobNameAndStatus") + } + + var r0 responses.MultiScheduleActionRecordsResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64, int64, int, int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX)); ok { + return rf(ctx, jobName, status, start, end, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64, int64, int, int) responses.MultiScheduleActionRecordsResponse); ok { + r0 = rf(ctx, jobName, status, start, end, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiScheduleActionRecordsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int64, int64, int, int) errors.EdgeX); ok { + r1 = rf(ctx, jobName, status, start, end, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// ScheduleActionRecordsByStatus provides a mock function with given fields: ctx, status, start, end, offset, limit +func (_m *ScheduleActionRecordClient) ScheduleActionRecordsByStatus(ctx context.Context, status string, start int64, end int64, offset int, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) { + ret := _m.Called(ctx, status, start, end, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for ScheduleActionRecordsByStatus") + } + + var r0 responses.MultiScheduleActionRecordsResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string, int64, int64, int, int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX)); ok { + return rf(ctx, status, start, end, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64, int64, int, int) responses.MultiScheduleActionRecordsResponse); ok { + r0 = rf(ctx, status, start, end, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiScheduleActionRecordsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64, int64, int, int) errors.EdgeX); ok { + r1 = rf(ctx, status, start, end, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// NewScheduleActionRecordClient creates a new instance of ScheduleActionRecordClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewScheduleActionRecordClient(t interface { + mock.TestingT + Cleanup(func()) +}) *ScheduleActionRecordClient { + mock := &ScheduleActionRecordClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/clients/interfaces/mocks/ScheduleJobClient.go b/clients/interfaces/mocks/ScheduleJobClient.go new file mode 100644 index 00000000..9304c018 --- /dev/null +++ b/clients/interfaces/mocks/ScheduleJobClient.go @@ -0,0 +1,220 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + common "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + + errors "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" + + mock "github.com/stretchr/testify/mock" + + requests "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests" + + responses "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" +) + +// ScheduleJobClient is an autogenerated mock type for the ScheduleJobClient type +type ScheduleJobClient struct { + mock.Mock +} + +// Add provides a mock function with given fields: ctx, reqs +func (_m *ScheduleJobClient) Add(ctx context.Context, reqs []requests.AddScheduleJobRequest) ([]common.BaseWithIdResponse, errors.EdgeX) { + ret := _m.Called(ctx, reqs) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 []common.BaseWithIdResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, []requests.AddScheduleJobRequest) ([]common.BaseWithIdResponse, errors.EdgeX)); ok { + return rf(ctx, reqs) + } + if rf, ok := ret.Get(0).(func(context.Context, []requests.AddScheduleJobRequest) []common.BaseWithIdResponse); ok { + r0 = rf(ctx, reqs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]common.BaseWithIdResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []requests.AddScheduleJobRequest) errors.EdgeX); ok { + r1 = rf(ctx, reqs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// AllScheduleJobs provides a mock function with given fields: ctx, offset, limit +func (_m *ScheduleJobClient) AllScheduleJobs(ctx context.Context, offset int, limit int) (responses.MultiScheduleJobsResponse, errors.EdgeX) { + ret := _m.Called(ctx, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for AllScheduleJobs") + } + + var r0 responses.MultiScheduleJobsResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, int, int) (responses.MultiScheduleJobsResponse, errors.EdgeX)); ok { + return rf(ctx, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int, int) responses.MultiScheduleJobsResponse); ok { + r0 = rf(ctx, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiScheduleJobsResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, int, int) errors.EdgeX); ok { + r1 = rf(ctx, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// DeleteScheduleJobByName provides a mock function with given fields: ctx, name +func (_m *ScheduleJobClient) DeleteScheduleJobByName(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteScheduleJobByName") + } + + var r0 common.BaseResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string) (common.BaseResponse, errors.EdgeX)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) common.BaseResponse); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(common.BaseResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { + r1 = rf(ctx, name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// ScheduleJobByName provides a mock function with given fields: ctx, name +func (_m *ScheduleJobClient) ScheduleJobByName(ctx context.Context, name string) (responses.ScheduleJobResponse, errors.EdgeX) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for ScheduleJobByName") + } + + var r0 responses.ScheduleJobResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string) (responses.ScheduleJobResponse, errors.EdgeX)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) responses.ScheduleJobResponse); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(responses.ScheduleJobResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { + r1 = rf(ctx, name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// TriggerScheduleJobByName provides a mock function with given fields: ctx, name +func (_m *ScheduleJobClient) TriggerScheduleJobByName(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for TriggerScheduleJobByName") + } + + var r0 common.BaseResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string) (common.BaseResponse, errors.EdgeX)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) common.BaseResponse); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(common.BaseResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { + r1 = rf(ctx, name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, reqs +func (_m *ScheduleJobClient) Update(ctx context.Context, reqs []requests.UpdateScheduleJobRequest) ([]common.BaseResponse, errors.EdgeX) { + ret := _m.Called(ctx, reqs) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 []common.BaseResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, []requests.UpdateScheduleJobRequest) ([]common.BaseResponse, errors.EdgeX)); ok { + return rf(ctx, reqs) + } + if rf, ok := ret.Get(0).(func(context.Context, []requests.UpdateScheduleJobRequest) []common.BaseResponse); ok { + r0 = rf(ctx, reqs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]common.BaseResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []requests.UpdateScheduleJobRequest) errors.EdgeX); ok { + r1 = rf(ctx, reqs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// NewScheduleJobClient creates a new instance of ScheduleJobClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewScheduleJobClient(t interface { + mock.TestingT + Cleanup(func()) +}) *ScheduleJobClient { + mock := &ScheduleJobClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/clients/interfaces/scheduleactionrecord.go b/clients/interfaces/scheduleactionrecord.go new file mode 100644 index 00000000..35a1234c --- /dev/null +++ b/clients/interfaces/scheduleactionrecord.go @@ -0,0 +1,27 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package interfaces + +import ( + "context" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +// ScheduleActionRecordClient defines the interface for interactions with the ScheduleActionRecord endpoint on the EdgeX Foundry support-cron-scheduler service. +type ScheduleActionRecordClient interface { + // AllScheduleActionRecords query schedule action records with start, end, offset, and limit + AllScheduleActionRecords(ctx context.Context, start, end int64, offset, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) + // LatestScheduleActionRecords query the latest schedule action records of each schedule job with offset, and limit + LatestScheduleActionRecords(ctx context.Context, offset, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) + // ScheduleActionRecordsByStatus queries schedule action records with status, start, end, offset, and limit + ScheduleActionRecordsByStatus(ctx context.Context, status string, start, end int64, offset, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) + // ScheduleActionRecordsByJobName query schedule action records with jobName, start, end, offset, and limit + ScheduleActionRecordsByJobName(ctx context.Context, jobName string, start, end int64, offset, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) + // ScheduleActionRecordsByJobNameAndStatus query schedule action records with jobName, status, start, end, offset, and limit + ScheduleActionRecordsByJobNameAndStatus(ctx context.Context, jobName, status string, start, end int64, offset, limit int) (responses.MultiScheduleActionRecordsResponse, errors.EdgeX) +} diff --git a/clients/interfaces/schedulejob.go b/clients/interfaces/schedulejob.go new file mode 100644 index 00000000..e633bd9b --- /dev/null +++ b/clients/interfaces/schedulejob.go @@ -0,0 +1,34 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package interfaces + +import ( + "context" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +// ScheduleJobClient defines the interface for interactions with the ScheduleJob endpoint on the EdgeX Foundry support-cron-scheduler service. +type ScheduleJobClient interface { + // Add adds new schedule jobs. + Add(ctx context.Context, reqs []requests.AddScheduleJobRequest) ([]common.BaseWithIdResponse, errors.EdgeX) + // Update updates schedule jobs. + Update(ctx context.Context, reqs []requests.UpdateScheduleJobRequest) ([]common.BaseResponse, errors.EdgeX) + // AllScheduleJobs returns all schedule jobs. + // The result can be limited in a certain range by specifying the offset and limit parameters. + // offset: The number of items to skip before starting to collect the result set. Default is 0. + // limit: The number of items to return. Specify -1 will return all remaining items after offset. The maximum will be the MaxResultCount as defined in the configuration of service. Default is 20. + AllScheduleJobs(ctx context.Context, offset int, limit int) (responses.MultiScheduleJobsResponse, errors.EdgeX) + // ScheduleJobByName returns a schedule job by name. + ScheduleJobByName(ctx context.Context, name string) (responses.ScheduleJobResponse, errors.EdgeX) + // DeleteScheduleJobByName deletes a schedule job by name. + DeleteScheduleJobByName(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) + // TriggerScheduleJobByName triggers a schedule job by name. + TriggerScheduleJobByName(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) +} diff --git a/common/constants.go b/common/constants.go index 9ba6f1a5..f9cd16c3 100644 --- a/common/constants.go +++ b/common/constants.go @@ -102,6 +102,19 @@ const ( ApiTransmissionByStatusRoute = ApiTransmissionRoute + "/" + Status + "/{" + Status + "}" ApiTransmissionByNotificationIdRoute = ApiTransmissionRoute + "/" + Notification + "/" + Id + "/{" + Id + "}" + ApiScheduleJobRoute = ApiBase + "/job" + ApiAllScheduleJobRoute = ApiScheduleJobRoute + "/" + All + ApiScheduleJobByNameRoute = ApiScheduleJobRoute + "/" + Name + "/{" + Name + "}" + ApiTriggerScheduleJobRoute = ApiScheduleJobRoute + "/" + Trigger + ApiTriggerScheduleJobByNameRoute = ApiScheduleJobRoute + "/" + Trigger + "/" + Name + "/{" + Name + "}" + + ApiScheduleActionRecordRoute = ApiBase + "/scheduleactionrecord" + ApiAllScheduleActionRecordRoute = ApiScheduleActionRecordRoute + "/" + All + ApiLatestScheduleActionRecordRoute = ApiScheduleActionRecordRoute + "/" + Latest + ApiScheduleActionRecordRouteByStatusRoute = ApiScheduleActionRecordRoute + "/" + Status + "/{" + Status + "}" + ApiScheduleActionRecordRouteByJobNameRoute = ApiScheduleActionRecordRoute + "/" + Job + "/" + Name + "/{" + Name + "}" + ApiScheduleActionRecordByJobNameAndStatusRoute = ApiScheduleActionRecordRoute + "/" + Job + "/" + Name + "/{" + Name + "}/" + Status + "/{" + Status + "}" + ApiConfigRoute = ApiBase + "/config" ApiPingRoute = ApiBase + "/ping" ApiVersionRoute = ApiBase + "/version" @@ -186,6 +199,9 @@ const ( Interval = "interval" Key = "key" ServiceId = "serviceId" + Job = "job" + Trigger = "trigger" + Latest = "latest" Offset = "offset" //query string to specify the number of items to skip before starting to collect the result set. Limit = "limit" //query string to specify the numbers of items to return @@ -270,6 +286,15 @@ const ( ReadWrite_WR = "WR" ) +// Constant for ScheduleJob +const ( + DefInterval = "INTERVAL" + DefCron = "CRON" + ActionEdgeXMessageBus = "EDGEXMESSAGEBUS" + ActionREST = "REST" + ActionDeviceControl = "DEVICECONTROL" +) + // Constants for Edgex Environment variable const ( EnvEncodeAllEvents = "EDGEX_ENCODE_ALL_EVENTS_CBOR" @@ -292,6 +317,7 @@ const ( SupportNotificationsServiceKey = "support-notifications" SystemManagementAgentServiceKey = "sys-mgmt-agent" SupportSchedulerServiceKey = "support-scheduler" + SupportCronSchedulerServiceKey = "support-cron-scheduler" SecuritySecretStoreSetupServiceKey = "security-secretstore-setup" SecurityProxyAuthServiceKey = "security-proxy-auth" SecurityProxySetupServiceKey = "security-proxy-setup" diff --git a/common/echo_api_constants.go b/common/echo_api_constants.go index 53b74f6b..f04d6a70 100644 --- a/common/echo_api_constants.go +++ b/common/echo_api_constants.go @@ -83,4 +83,16 @@ const ( ApiKVSByKeyEchoRoute = ApiKVSRoute + "/" + Key + "/:" + Key ApiRegistrationByServiceIdEchoRoute = ApiRegisterRoute + "/" + ServiceId + "/:" + ServiceId + + ApiScheduleJobEchoRoute = ApiBase + "/job" + ApiAllScheduleJobEchoRoute = ApiScheduleJobRoute + "/" + All + ApiTriggerScheduleJobByNameEchoRoute = ApiScheduleJobRoute + "/" + Trigger + "/" + Name + "/:" + Name + ApiScheduleJobByNameEchoRoute = ApiScheduleJobRoute + "/" + Name + "/:" + Name + + ApiScheduleActionRecordEchoRoute = ApiBase + "/scheduleactionrecord" + ApiAllScheduleActionRecordEchoRoute = ApiScheduleActionRecordRoute + "/" + All + ApiLatestScheduleActionRecordEchoRoute = ApiScheduleActionRecordRoute + "/" + Latest + ApiScheduleActionRecordRouteByStatusEchoRoute = ApiScheduleActionRecordRoute + "/" + Status + "/:" + Status + ApiScheduleActionRecordRouteByJobNameEchoRoute = ApiScheduleActionRecordRoute + "/" + Job + "/" + Name + "/:" + Name + ApiScheduleActionRecordRouteByJobNameAndStatusEchoRoute = ApiScheduleActionRecordRoute + "/" + Job + "/" + Name + "/:" + Name + Status + "/:" + Status ) diff --git a/dtos/requests/schedulejob.go b/dtos/requests/schedulejob.go new file mode 100644 index 00000000..6c4001e6 --- /dev/null +++ b/dtos/requests/schedulejob.go @@ -0,0 +1,124 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package requests + +import ( + "encoding/json" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +// AddScheduleJobRequest defines the Request Content for POST ScheduleJob DTO. +type AddScheduleJobRequest struct { + dtoCommon.BaseRequest `json:",inline"` + ScheduleJob dtos.ScheduleJob `json:"scheduleJob"` +} + +// Validate satisfies the Validator interface +func (a *AddScheduleJobRequest) Validate() error { + err := common.Validate(a) + if err != nil { + return err + } + + err = a.ScheduleJob.Validate() + if err != nil { + return err + } + return nil +} + +// UnmarshalJSON implements the Unmarshaler interface for the AddScheduleJobRequest type +func (a *AddScheduleJobRequest) UnmarshalJSON(b []byte) error { + var alias struct { + dtoCommon.BaseRequest + ScheduleJob dtos.ScheduleJob + } + if err := json.Unmarshal(b, &alias); err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal request body as JSON.", err) + } + + *a = AddScheduleJobRequest(alias) + + // validate AddScheduleJobRequest DTO + if err := a.Validate(); err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + return nil +} + +// UpdateScheduleJobRequest defines the Request Content for PUT event as pushed DTO. +type UpdateScheduleJobRequest struct { + dtoCommon.BaseRequest `json:",inline"` + ScheduleJob dtos.UpdateScheduleJob `json:"scheduleJob"` +} + +// Validate satisfies the Validator interface +func (a *UpdateScheduleJobRequest) Validate() error { + err := common.Validate(a) + if err != nil { + return err + } + + return nil +} + +// UnmarshalJSON implements the Unmarshaler interface for the UpdateScheduleJobRequest type +func (u *UpdateScheduleJobRequest) UnmarshalJSON(b []byte) error { + var alias struct { + dtoCommon.BaseRequest + ScheduleJob dtos.UpdateScheduleJob + } + if err := json.Unmarshal(b, &alias); err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal request body as JSON.", err) + } + + *u = UpdateScheduleJobRequest(alias) + + // validate AddScheduleJobRequest DTO + if err := u.Validate(); err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + return nil +} + +// ReplaceScheduleJobModelFieldsWithDTO replace existing ScheduleJob's fields with DTO patch +func ReplaceScheduleJobModelFieldsWithDTO(ds *models.ScheduleJob, patch dtos.UpdateScheduleJob) { + if patch.Actions != nil { + ds.Actions = dtos.ToScheduleActionModels(patch.Actions) + } + if patch.AdminState != nil { + ds.AdminState = models.AdminState(*patch.AdminState) + } + if patch.Labels != nil { + ds.Labels = patch.Labels + } + if patch.Definition != nil { + ds.Definition = dtos.ToScheduleDefModel(*patch.Definition) + } + if patch.Properties != nil { + ds.Properties = patch.Properties + } +} + +// NewAddScheduleJobRequest creates, initializes and returns an AddScheduleJobRequest +func NewAddScheduleJobRequest(dto dtos.ScheduleJob) AddScheduleJobRequest { + return AddScheduleJobRequest{ + BaseRequest: dtoCommon.NewBaseRequest(), + ScheduleJob: dto, + } +} + +func NewUpdateScheduleJobRequest(dto dtos.UpdateScheduleJob) UpdateScheduleJobRequest { + return UpdateScheduleJobRequest{ + BaseRequest: dtoCommon.NewBaseRequest(), + ScheduleJob: dto, + } +} diff --git a/dtos/requests/schedulejob_test.go b/dtos/requests/schedulejob_test.go new file mode 100644 index 00000000..869d5928 --- /dev/null +++ b/dtos/requests/schedulejob_test.go @@ -0,0 +1,264 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package requests + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +var ( + testScheduleJonName = "jobName" + testScheduleJobLabels = []string{"label"} + testScheduleDef = dtos.ScheduleDef{ + Type: common.DefInterval, + IntervalScheduleDef: dtos.IntervalScheduleDef{ + Interval: "10m", + }, + } + testScheduleActions = []dtos.ScheduleAction{ + { + Type: common.ActionEdgeXMessageBus, + ContentType: common.ContentTypeJSON, + Payload: nil, + EdgeXMessageBusAction: dtos.EdgeXMessageBusAction{ + Topic: "testTopic", + }, + }, + { + Type: common.ActionREST, + ContentType: common.ContentTypeJSON, + Payload: nil, + RESTAction: dtos.RESTAction{ + Address: "testAddress", + }, + }, + } +) + +func addScheduleJobRequestData() AddScheduleJobRequest { + return NewAddScheduleJobRequest(dtos.ScheduleJob{ + Name: testScheduleJonName, + Definition: testScheduleDef, + Actions: testScheduleActions, + AdminState: models.Unlocked, + Labels: testScheduleJobLabels, + }) +} + +func updateScheduleJobData() dtos.UpdateScheduleJob { + id := ExampleUUID + name := testScheduleJonName + definition := testScheduleDef + actions := testScheduleActions + labels := testScheduleJobLabels + return dtos.UpdateScheduleJob{ + Id: &id, + Name: &name, + Definition: &definition, + Actions: actions, + Labels: labels, + } +} + +func TestAddScheduleJobRequest_Validate(t *testing.T) { + emptyString := " " + valid := addScheduleJobRequestData() + noReqId := addScheduleJobRequestData() + noReqId.RequestId = "" + invalidReqId := addScheduleJobRequestData() + invalidReqId.RequestId = "abc" + + noScheduleJobName := addScheduleJobRequestData() + noScheduleJobName.ScheduleJob.Name = emptyString + ScheduleJobNameWithReservedChars := addScheduleJobRequestData() + ScheduleJobNameWithReservedChars.ScheduleJob.Name = namesWithReservedChar[0] + + noDefinition := addScheduleJobRequestData() + noDefinition.ScheduleJob.Actions = nil + unsupportedDefinitionType := addScheduleJobRequestData() + unsupportedDefinitionType.ScheduleJob.Definition = dtos.ScheduleDef{ + Type: "unknown", + } + + noActions := addScheduleJobRequestData() + noActions.ScheduleJob.Actions = nil + unsupportedActionType := addScheduleJobRequestData() + unsupportedActionType.ScheduleJob.Actions = []dtos.ScheduleAction{ + {Type: "unknown"}, + } + + tests := []struct { + name string + ScheduleJob AddScheduleJobRequest + expectError bool + }{ + {"valid", valid, false}, + {"valid, no request ID", noReqId, false}, + {"invalid, request ID is not an UUID", invalidReqId, true}, + {"invalid, no schedule job name", noScheduleJobName, true}, + {"valid, schedule job name containing reserved chars", ScheduleJobNameWithReservedChars, false}, + {"invalid, no definition specified", noDefinition, true}, + {"invalid, unsupported definition type", unsupportedDefinitionType, true}, + {"invalid, no actions specified", noActions, true}, + {"invalid, unsupported action type", unsupportedActionType, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.ScheduleJob.Validate() + assert.Equal(t, tt.expectError, err != nil, "Unexpected AddScheduleJobRequest validation result.", err) + }) + } +} + +func TestAddScheduleJobRequest_UnmarshalJSON(t *testing.T) { + validAddScheduleJobRequest := addScheduleJobRequestData() + jsonData, _ := json.Marshal(validAddScheduleJobRequest) + tests := []struct { + name string + expected AddScheduleJobRequest + data []byte + wantErr bool + }{ + {"unmarshal AddScheduleJobRequest with success", validAddScheduleJobRequest, jsonData, false}, + {"unmarshal invalid AddScheduleJobRequest, empty data", AddScheduleJobRequest{}, []byte{}, true}, + {"unmarshal invalid AddScheduleJobRequest, string data", AddScheduleJobRequest{}, []byte("Invalid AddScheduleJobRequest"), true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result AddScheduleJobRequest + err := result.UnmarshalJSON(tt.data) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result, "Unmarshal did not result in expected AddScheduleJobRequest.") + } + }) + } +} + +func TestUpdateScheduleJobRequest_Validate(t *testing.T) { + emptyString := " " + invalidUUID := "invalidUUID" + + valid := NewUpdateScheduleJobRequest(updateScheduleJobData()) + noReqId := valid + noReqId.RequestId = "" + invalidReqId := valid + invalidReqId.RequestId = invalidUUID + + validOnlyId := valid + validOnlyId.ScheduleJob.Name = nil + invalidId := valid + invalidId.ScheduleJob.Id = &invalidUUID + + validOnlyName := valid + validOnlyName.ScheduleJob.Id = nil + nameAndEmptyId := valid + nameAndEmptyId.ScheduleJob.Id = &emptyString + invalidEmptyName := valid + invalidEmptyName.ScheduleJob.Name = &emptyString + + unsupportedDefinitionType := NewUpdateScheduleJobRequest(updateScheduleJobData()) + unsupportedDefinition := dtos.ScheduleDef{ + Type: "unknown", + } + unsupportedDefinitionType.ScheduleJob.Definition = &unsupportedDefinition + validWithoutDefinition := NewUpdateScheduleJobRequest(updateScheduleJobData()) + validWithoutDefinition.ScheduleJob.Definition = nil + invalidEmptyDefinition := NewUpdateScheduleJobRequest(updateScheduleJobData()) + emptyDefinition := dtos.ScheduleDef{} + invalidEmptyDefinition.ScheduleJob.Definition = &emptyDefinition + + noActions := NewUpdateScheduleJobRequest(updateScheduleJobData()) + noActions.ScheduleJob.Actions = nil + noLabels := NewUpdateScheduleJobRequest(updateScheduleJobData()) + noLabels.ScheduleJob.Labels = nil + + emptyActions := NewUpdateScheduleJobRequest(updateScheduleJobData()) + emptyActions.ScheduleJob.Actions = []dtos.ScheduleAction{} + emptyLabels := NewUpdateScheduleJobRequest(updateScheduleJobData()) + emptyLabels.ScheduleJob.Labels = []string{} + + tests := []struct { + name string + req UpdateScheduleJobRequest + expectError bool + }{ + {"valid", valid, false}, + {"valid, no request ID", noReqId, false}, + {"invalid, request ID is not an UUID", invalidReqId, true}, + {"valid, only ID", validOnlyId, false}, + {"invalid, invalid ID", invalidId, true}, + {"valid, only name", validOnlyName, false}, + {"valid, name and empty Id", nameAndEmptyId, false}, + {"invalid, empty name", invalidEmptyName, true}, + {"invalid, unsupported definition type", unsupportedDefinitionType, true}, + {"valid, without definition", validWithoutDefinition, false}, + {"invalid, empty definition", invalidEmptyDefinition, true}, + {"valid, no actions", noActions, false}, + {"valid, no labels", noLabels, false}, + {"valid, empty actions", emptyActions, false}, + {"valid, empty labels", emptyLabels, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + assert.Equal(t, tt.expectError, err != nil, "Unexpected UpdateScheduleJobRequest validation result.", err) + }) + } +} + +func TestUpdateScheduleJobRequest_UnmarshalJSON(t *testing.T) { + validUpdateScheduleJobRequest := NewUpdateScheduleJobRequest(updateScheduleJobData()) + jsonData, _ := json.Marshal(validUpdateScheduleJobRequest) + tests := []struct { + name string + expected UpdateScheduleJobRequest + data []byte + wantErr bool + }{ + {"unmarshal UpdateScheduleJobRequest with success", validUpdateScheduleJobRequest, jsonData, false}, + {"unmarshal invalid UpdateScheduleJobRequest, empty data", UpdateScheduleJobRequest{}, []byte{}, true}, + {"unmarshal invalid UpdateScheduleJobRequest, string data", UpdateScheduleJobRequest{}, []byte("Invalid UpdateScheduleJobRequest"), true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result UpdateScheduleJobRequest + err := result.UnmarshalJSON(tt.data) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result, "Unmarshal did not result in expected UpdateScheduleJobRequest.", err) + } + }) + } +} + +func TestReplaceScheduleJobModelFieldsWithDTO(t *testing.T) { + job := models.ScheduleJob{ + Id: "7a1707f0-166f-4c4b-bc9d-1d54c74e0137", + Name: testScheduleJonName, + } + patch := updateScheduleJobData() + + ReplaceScheduleJobModelFieldsWithDTO(&job, patch) + + expectedActions := dtos.ToScheduleActionModels(patch.Actions) + expectedDef := dtos.ToScheduleDefModel(*patch.Definition) + assert.Equal(t, testScheduleJonName, job.Name) + assert.Equal(t, expectedActions, job.Actions) + assert.Equal(t, expectedDef, job.Definition) +} diff --git a/dtos/responses/scheduleactionrecord.go b/dtos/responses/scheduleactionrecord.go new file mode 100644 index 00000000..319a64af --- /dev/null +++ b/dtos/responses/scheduleactionrecord.go @@ -0,0 +1,37 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package responses + +import ( + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" +) + +// ScheduleActionRecordResponse defines the Response Content for GET ScheduleActionRecord DTO. +type ScheduleActionRecordResponse struct { + common.BaseResponse `json:",inline"` + ScheduleActionRecord dtos.ScheduleActionRecord `json:"scheduleActionRecord"` +} + +func NewScheduleActionRecordResponse(requestId string, message string, statusCode int, scheduleActionRecord dtos.ScheduleActionRecord) ScheduleActionRecordResponse { + return ScheduleActionRecordResponse{ + BaseResponse: common.NewBaseResponse(requestId, message, statusCode), + ScheduleActionRecord: scheduleActionRecord, + } +} + +// MultiScheduleActionRecordsResponse defines the Response Content for GET multiple ScheduleActionRecord DTOs. +type MultiScheduleActionRecordsResponse struct { + common.BaseWithTotalCountResponse `json:",inline"` + ScheduleActionRecords []dtos.ScheduleActionRecord `json:"scheduleActionRecords"` +} + +func NewMultiScheduleActionRecordsResponse(requestId string, message string, statusCode int, totalCount uint32, scheduleActionRecords []dtos.ScheduleActionRecord) MultiScheduleActionRecordsResponse { + return MultiScheduleActionRecordsResponse{ + BaseWithTotalCountResponse: common.NewBaseWithTotalCountResponse(requestId, message, statusCode, totalCount), + ScheduleActionRecords: scheduleActionRecords, + } +} diff --git a/dtos/responses/scheduleactionrecord_test.go b/dtos/responses/scheduleactionrecord_test.go new file mode 100644 index 00000000..e6ca6df9 --- /dev/null +++ b/dtos/responses/scheduleactionrecord_test.go @@ -0,0 +1,50 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package responses + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" +) + +func TestNewScheduleActionRecordResponse(t *testing.T) { + expectedRequestId := "123456" + expectedStatusCode := http.StatusOK + expectedMessage := "unit test message" + expectedScheduleActionRecord := dtos.ScheduleActionRecord{JobName: "testJob"} + actual := NewScheduleActionRecordResponse(expectedRequestId, expectedMessage, expectedStatusCode, expectedScheduleActionRecord) + + assert.Equal(t, expectedRequestId, actual.RequestId) + assert.Equal(t, expectedStatusCode, actual.StatusCode) + assert.Equal(t, expectedMessage, actual.Message) + assert.Equal(t, expectedScheduleActionRecord, actual.ScheduleActionRecord) +} + +func TestNewMultiScheduleActionRecordsResponse(t *testing.T) { + expectedRequestId := "123456" + expectedStatusCode := http.StatusOK + expectedMessage := "unit test message" + expectedScheduleActionRecords := []dtos.ScheduleActionRecord{ + { + JobName: "testJob1", + }, + { + JobName: "testJob2", + }, + } + expectedTotalCount := uint32(2) + actual := NewMultiScheduleActionRecordsResponse(expectedRequestId, expectedMessage, expectedStatusCode, uint32(len(expectedScheduleActionRecords)), expectedScheduleActionRecords) + + assert.Equal(t, expectedRequestId, actual.RequestId) + assert.Equal(t, expectedStatusCode, actual.StatusCode) + assert.Equal(t, expectedMessage, actual.Message) + assert.Equal(t, expectedTotalCount, actual.TotalCount) + assert.Equal(t, expectedScheduleActionRecords, actual.ScheduleActionRecords) +} diff --git a/dtos/responses/schedulejob.go b/dtos/responses/schedulejob.go new file mode 100644 index 00000000..15c744d5 --- /dev/null +++ b/dtos/responses/schedulejob.go @@ -0,0 +1,37 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package responses + +import ( + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" +) + +// ScheduleJobResponse defines the Response Content for GET ScheduleJob DTO. +type ScheduleJobResponse struct { + common.BaseResponse `json:",inline"` + ScheduleJob dtos.ScheduleJob `json:"scheduleJob"` +} + +func NewScheduleJobResponse(requestId string, message string, statusCode int, scheduleJob dtos.ScheduleJob) ScheduleJobResponse { + return ScheduleJobResponse{ + BaseResponse: common.NewBaseResponse(requestId, message, statusCode), + ScheduleJob: scheduleJob, + } +} + +// MultiScheduleJobsResponse defines the Response Content for GET multiple ScheduleJob DTOs. +type MultiScheduleJobsResponse struct { + common.BaseWithTotalCountResponse `json:",inline"` + ScheduleJobs []dtos.ScheduleJob `json:"scheduleJobs"` +} + +func NewMultiScheduleJobsResponse(requestId string, message string, statusCode int, totalCount uint32, scheduleJobs []dtos.ScheduleJob) MultiScheduleJobsResponse { + return MultiScheduleJobsResponse{ + BaseWithTotalCountResponse: common.NewBaseWithTotalCountResponse(requestId, message, statusCode, totalCount), + ScheduleJobs: scheduleJobs, + } +} diff --git a/dtos/responses/schedulejob_test.go b/dtos/responses/schedulejob_test.go new file mode 100644 index 00000000..3f1431fb --- /dev/null +++ b/dtos/responses/schedulejob_test.go @@ -0,0 +1,46 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package responses + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" +) + +func TestNewScheduleJobResponse(t *testing.T) { + expectedRequestId := "123456" + expectedStatusCode := http.StatusOK + expectedMessage := "unit test message" + expectedScheduleJob := dtos.ScheduleJob{Name: "testJob"} + actual := NewScheduleJobResponse(expectedRequestId, expectedMessage, expectedStatusCode, expectedScheduleJob) + + assert.Equal(t, expectedRequestId, actual.RequestId) + assert.Equal(t, expectedStatusCode, actual.StatusCode) + assert.Equal(t, expectedMessage, actual.Message) + assert.Equal(t, expectedScheduleJob, actual.ScheduleJob) +} + +func TestNewMultiScheduleJobsResponse(t *testing.T) { + expectedRequestId := "123456" + expectedStatusCode := http.StatusOK + expectedMessage := "unit test message" + expectedScheduleJobs := []dtos.ScheduleJob{ + {Name: "testJob1"}, + {Name: "testJob2"}, + } + expectedTotalCount := uint32(2) + actual := NewMultiScheduleJobsResponse(expectedRequestId, expectedMessage, expectedStatusCode, uint32(len(expectedScheduleJobs)), expectedScheduleJobs) + + assert.Equal(t, expectedRequestId, actual.RequestId) + assert.Equal(t, expectedStatusCode, actual.StatusCode) + assert.Equal(t, expectedMessage, actual.Message) + assert.Equal(t, expectedTotalCount, actual.TotalCount) + assert.Equal(t, expectedScheduleJobs, actual.ScheduleJobs) +} diff --git a/dtos/scheduleactionrecord.go b/dtos/scheduleactionrecord.go new file mode 100644 index 00000000..b0841526 --- /dev/null +++ b/dtos/scheduleactionrecord.go @@ -0,0 +1,60 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package dtos + +import ( + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +type ScheduleActionRecord struct { + Id string `json:"id,omitempty" validate:"omitempty,uuid"` + JobName string `json:"jobName" validate:"edgex-dto-none-empty-string"` + Action ScheduleAction `json:"action" validate:"required"` + Status string `json:"status" validate:"required,oneof='SUCCEEDED' 'FAILED' 'MISSED'"` + ScheduledAt int64 `json:"scheduledAt,omitempty"` + Created int64 `json:"created,omitempty"` +} + +// Validate satisfies the Validator interface +func (c *ScheduleActionRecord) Validate() error { + err := common.Validate(c) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleActionRecord.", err) + } + + err = c.Action.Validate() + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleAction.", err) + } + + return nil +} + +func ToScheduleActionRecordModel(dto ScheduleActionRecord) models.ScheduleActionRecord { + var model models.ScheduleActionRecord + model.Id = dto.Id + model.JobName = dto.JobName + model.Action = ToScheduleActionModel(dto.Action) + model.Status = models.ScheduleActionRecordStatus(dto.Status) + model.ScheduledAt = dto.ScheduledAt + model.Created = dto.Created + + return model +} + +func FromScheduleActionRecordModelToDTO(model models.ScheduleActionRecord) ScheduleActionRecord { + var dto ScheduleActionRecord + dto.Id = model.Id + dto.JobName = model.JobName + dto.Action = FromScheduleActionModelToDTO(model.Action) + dto.Status = string(model.Status) + dto.ScheduledAt = model.ScheduledAt + dto.Created = model.Created + + return dto +} diff --git a/dtos/scheduleactionrecord_test.go b/dtos/scheduleactionrecord_test.go new file mode 100644 index 00000000..08472cbb --- /dev/null +++ b/dtos/scheduleactionrecord_test.go @@ -0,0 +1,85 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package dtos + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +var ( + scheduleActionRecord = ScheduleActionRecord{ + Id: TestUUID, + JobName: jobName, + Action: scheduleActionEdgeXMessageBus, + Status: models.Missed, + ScheduledAt: TestTimestamp, + Created: TestTimestamp, + } + scheduleActionRecordModel = models.ScheduleActionRecord{ + Id: TestUUID, + JobName: jobName, + Action: scheduleActionEdgeXMessageBusModel, + Status: models.Missed, + ScheduledAt: TestTimestamp, + Created: TestTimestamp, + } +) + +func TestScheduleActionRecord_Validate(t *testing.T) { + validScheduleActionRecord := scheduleActionRecord + invalidId := scheduleActionRecord + invalidId.Id = "123" + emptyJobName := scheduleActionRecord + emptyJobName.JobName = "" + emptyAction := scheduleActionRecord + emptyAction.Action = ScheduleAction{} + invalidAction := scheduleActionRecord + invalidAction.Action = ScheduleAction{ + Type: common.ActionEdgeXMessageBus, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + } + invalidStatus := scheduleActionRecord + invalidStatus.Status = "xxx" + + tests := []struct { + name string + request ScheduleActionRecord + expectedErr bool + }{ + {"valid ScheduleActionRecord", validScheduleActionRecord, false}, + {"invalid ScheduleActionRecord, invalid ID", invalidId, true}, + {"invalid ScheduleActionRecord, empty JobName", emptyJobName, true}, + {"invalid ScheduleActionRecord, empty Action", emptyAction, true}, + {"invalid ScheduleActionRecord, invalid Action", invalidAction, true}, + {"invalid ScheduleActionRecord, invalid Status", invalidStatus, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.request.Validate() + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestToScheduleActionRecordModel(t *testing.T) { + result := ToScheduleActionRecordModel(scheduleActionRecord) + assert.Equal(t, scheduleActionRecordModel, result, "ToScheduleActionRecordModel did not result in ScheduleActionRecord model") +} + +func TestFromScheduleActionRecordModelToDTO(t *testing.T) { + result := FromScheduleActionRecordModelToDTO(scheduleActionRecordModel) + assert.Equal(t, scheduleActionRecord, result, "FromScheduleActionRecordModelToDTO did not result in ScheduleActionRecord dto") +} diff --git a/dtos/schedulejob.go b/dtos/schedulejob.go new file mode 100644 index 00000000..2c36adb5 --- /dev/null +++ b/dtos/schedulejob.go @@ -0,0 +1,306 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package dtos + +import ( + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +type ScheduleJob struct { + DBTimestamp `json:",inline"` + Id string `json:"id,omitempty" validate:"omitempty,uuid"` + Name string `json:"name" validate:"edgex-dto-none-empty-string"` + Definition ScheduleDef `json:"definition" validate:"required"` + Actions []ScheduleAction `json:"actions" validate:"required,gt=0,dive"` + AdminState string `json:"adminState" validate:"oneof='LOCKED' 'UNLOCKED'"` + Labels []string `json:"labels,omitempty"` + Properties map[string]any `json:"properties,omitempty"` +} + +type UpdateScheduleJob struct { + Id *string `json:"id" validate:"required_without=Name,edgex-dto-uuid"` + Name *string `json:"name" validate:"required_without=Id,edgex-dto-none-empty-string"` + Definition *ScheduleDef `json:"definition" validate:"omitempty"` + Actions []ScheduleAction `json:"actions,omitempty"` + AdminState *string `json:"adminState" validate:"omitempty,oneof='LOCKED' 'UNLOCKED'"` + Labels []string `json:"labels,omitempty"` + Properties map[string]any `json:"properties,omitempty"` +} + +// Validate satisfies the Validator interface +func (s *ScheduleJob) Validate() error { + err := common.Validate(s) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleJob.", err) + } + + err = s.Definition.Validate() + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleDef.", err) + } + + for _, action := range s.Actions { + err = action.Validate() + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleAction.", err) + } + } + + return nil +} + +type ScheduleDef struct { + Type string `json:"type" validate:"oneof='INTERVAL' 'CRON'"` + + IntervalScheduleDef `json:",inline" validate:"-"` + CronScheduleDef `json:",inline" validate:"-"` +} + +// Validate satisfies the Validator interface +func (s *ScheduleDef) Validate() error { + err := common.Validate(s) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleDef.", err) + } + + switch s.Type { + case common.DefInterval: + err = common.Validate(s.IntervalScheduleDef) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid IntervalScheduleDef.", err) + } + case common.DefCron: + err = common.Validate(s.CronScheduleDef) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid CronScheduleDef.", err) + } + } + + return nil +} + +type IntervalScheduleDef struct { + Interval string `json:"duration" validate:"required,edgex-dto-duration"` +} + +type CronScheduleDef struct { + Crontab string `json:"crontab" validate:"required"` +} + +type ScheduleAction struct { + Type string `json:"type" validate:"oneof='EDGEXMESSAGEBUS' 'REST' 'DEVICECONTROL'"` + ContentType string `json:"contentType,omitempty"` + Payload []byte `json:"payload,omitempty"` + + EdgeXMessageBusAction `json:",inline" validate:"-"` + RESTAction `json:",inline" validate:"-"` + DeviceControlAction `json:",inline" validate:"-"` +} + +func (s *ScheduleAction) Validate() error { + err := common.Validate(s) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid ScheduleAction.", err) + } + + switch s.Type { + case common.ActionEdgeXMessageBus: + err = common.Validate(s.EdgeXMessageBusAction) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid EdgeXMessageBusAction.", err) + } + case common.ActionREST: + err = common.Validate(s.RESTAction) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid RESTAction.", err) + } + case common.ActionDeviceControl: + err = common.Validate(s.DeviceControlAction) + if err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "invalid DeviceControlAction.", err) + } + } + + return nil +} + +type EdgeXMessageBusAction struct { + Topic string `json:"topic" validate:"required"` +} + +type RESTAction struct { + Address string `json:"address" validate:"required"` + InjectEdgeXAuth bool `json:"injectEdgeXAuth,omitempty"` +} + +type DeviceControlAction struct { + DeviceName string `json:"deviceName" validate:"required"` + SourceName string `json:"sourceName" validate:"required"` +} + +func ToScheduleJobModel(dto ScheduleJob) models.ScheduleJob { + var model models.ScheduleJob + model.Id = dto.Id + model.Name = dto.Name + model.Definition = ToScheduleDefModel(dto.Definition) + model.Actions = ToScheduleActionModels(dto.Actions) + model.AdminState = models.AdminState(dto.AdminState) + model.Labels = dto.Labels + model.Properties = dto.Properties + + return model +} + +func FromScheduleJobModelToDTO(model models.ScheduleJob) ScheduleJob { + var dto ScheduleJob + dto.DBTimestamp = DBTimestamp(model.DBTimestamp) + dto.Id = model.Id + dto.Name = model.Name + dto.Definition = FromScheduleDefModelToDTO(model.Definition) + dto.Actions = FromScheduleActionModelsToDTOs(model.Actions) + dto.AdminState = string(model.AdminState) + dto.Labels = model.Labels + dto.Properties = model.Properties + + return dto +} + +func ToScheduleDefModel(dto ScheduleDef) models.ScheduleDef { + var model models.ScheduleDef + + switch dto.Type { + case common.DefInterval: + model = models.IntervalScheduleDef{ + BaseScheduleDef: models.BaseScheduleDef{Type: common.DefInterval}, + Interval: dto.Interval, + } + case common.DefCron: + model = models.CronScheduleDef{ + BaseScheduleDef: models.BaseScheduleDef{Type: common.DefCron}, + Crontab: dto.Crontab, + } + } + + return model +} + +func FromScheduleDefModelToDTO(model models.ScheduleDef) ScheduleDef { + var dto ScheduleDef + + switch model.GetBaseScheduleDef().Type { + case common.DefInterval: + durationModel := model.(models.IntervalScheduleDef) + dto = ScheduleDef{ + Type: common.DefInterval, + IntervalScheduleDef: IntervalScheduleDef{Interval: durationModel.Interval}, + } + case common.DefCron: + cronModel := model.(models.CronScheduleDef) + dto = ScheduleDef{ + Type: common.DefCron, + CronScheduleDef: CronScheduleDef{Crontab: cronModel.Crontab}, + } + } + + return dto +} + +func ToScheduleActionModel(dto ScheduleAction) models.ScheduleAction { + var model models.ScheduleAction + + switch dto.Type { + case common.ActionEdgeXMessageBus: + model = models.EdgeXMessageBusAction{ + BaseScheduleAction: models.BaseScheduleAction{ + Type: common.ActionEdgeXMessageBus, + ContentType: dto.ContentType, + Payload: dto.Payload, + }, + Topic: dto.Topic, + } + case common.ActionREST: + model = models.RESTAction{ + BaseScheduleAction: models.BaseScheduleAction{ + Type: common.ActionREST, + ContentType: dto.ContentType, + Payload: dto.Payload, + }, + Address: dto.Address, + InjectEdgeXAuth: dto.InjectEdgeXAuth, + } + case common.ActionDeviceControl: + model = models.DeviceControlAction{ + BaseScheduleAction: models.BaseScheduleAction{ + Type: common.ActionDeviceControl, + ContentType: dto.ContentType, + Payload: dto.Payload, + }, + DeviceName: dto.DeviceName, + SourceName: dto.SourceName, + } + } + + return model +} + +func FromScheduleActionModelToDTO(model models.ScheduleAction) ScheduleAction { + var dto ScheduleAction + + switch model.GetBaseScheduleAction().Type { + case common.ActionEdgeXMessageBus: + messageBusModel := model.(models.EdgeXMessageBusAction) + dto = ScheduleAction{ + Type: common.ActionEdgeXMessageBus, + ContentType: messageBusModel.ContentType, + Payload: messageBusModel.Payload, + EdgeXMessageBusAction: EdgeXMessageBusAction{ + Topic: messageBusModel.Topic, + }, + } + case common.ActionREST: + restModel := model.(models.RESTAction) + dto = ScheduleAction{ + Type: common.ActionREST, + ContentType: restModel.ContentType, + Payload: restModel.Payload, + RESTAction: RESTAction{ + Address: restModel.Address, + InjectEdgeXAuth: restModel.InjectEdgeXAuth, + }, + } + case common.ActionDeviceControl: + deviceControlModel := model.(models.DeviceControlAction) + dto = ScheduleAction{ + Type: common.ActionDeviceControl, + ContentType: deviceControlModel.ContentType, + Payload: deviceControlModel.Payload, + DeviceControlAction: DeviceControlAction{ + DeviceName: deviceControlModel.DeviceName, + SourceName: deviceControlModel.SourceName, + }, + } + } + + return dto +} + +func ToScheduleActionModels(dtos []ScheduleAction) []models.ScheduleAction { + models := make([]models.ScheduleAction, len(dtos)) + for i, dto := range dtos { + models[i] = ToScheduleActionModel(dto) + } + return models +} + +func FromScheduleActionModelsToDTOs(models []models.ScheduleAction) []ScheduleAction { + dtos := make([]ScheduleAction, len(models)) + for i, model := range models { + dtos[i] = FromScheduleActionModelToDTO(model) + } + return dtos +} diff --git a/dtos/schedulejob_test.go b/dtos/schedulejob_test.go new file mode 100644 index 00000000..4ca3df0e --- /dev/null +++ b/dtos/schedulejob_test.go @@ -0,0 +1,253 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package dtos + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" +) + +const ( + jobName = "mock-job-name" + payload = "eyJ0ZXN0I" + topic = "mock-topic" + crontab = "0 0 0 1 1 *" +) + +var scheduleActionEdgeXMessageBus = ScheduleAction{ + Type: common.ActionEdgeXMessageBus, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + EdgeXMessageBusAction: EdgeXMessageBusAction{ + Topic: topic, + }, +} + +var scheduleActionEdgeXMessageBusModel = models.EdgeXMessageBusAction{ + BaseScheduleAction: models.BaseScheduleAction{ + Type: common.ActionEdgeXMessageBus, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + }, + Topic: topic, +} + +var scheduleActionRest = ScheduleAction{ + Type: common.ActionREST, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + RESTAction: RESTAction{ + Address: testPath, + }, +} + +var scheduleActionRestModel = models.RESTAction{ + BaseScheduleAction: models.BaseScheduleAction{ + Type: common.ActionREST, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + }, + Address: testPath, +} + +var scheduleActionDeviceControl = ScheduleAction{ + Type: common.ActionDeviceControl, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + DeviceControlAction: DeviceControlAction{ + DeviceName: TestDeviceName, + SourceName: TestSourceName, + }, +} + +var scheduleActionDeviceControlModel = models.DeviceControlAction{ + BaseScheduleAction: models.BaseScheduleAction{ + Type: common.ActionDeviceControl, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + }, + DeviceName: TestDeviceName, + SourceName: TestSourceName, +} + +var scheduleIntervalDef = ScheduleDef{ + Type: common.DefInterval, + IntervalScheduleDef: IntervalScheduleDef{ + Interval: interval, + }, +} + +var scheduleIntervalDefModel = models.IntervalScheduleDef{ + BaseScheduleDef: models.BaseScheduleDef{ + Type: common.DefInterval, + }, + Interval: interval, +} + +var scheduleCronDef = ScheduleDef{ + Type: common.DefCron, + CronScheduleDef: CronScheduleDef{ + Crontab: crontab, + }, +} + +var scheduleCronDefModel = models.CronScheduleDef{ + BaseScheduleDef: models.BaseScheduleDef{ + Type: common.DefCron, + }, + Crontab: crontab, +} + +var ( + scheduleJob = ScheduleJob{ + DBTimestamp: DBTimestamp{}, + Id: TestUUID, + Name: jobName, + Definition: scheduleIntervalDef, + Actions: []ScheduleAction{scheduleActionEdgeXMessageBus}, + AdminState: testAdminState, + } + scheduleJobModel = models.ScheduleJob{ + DBTimestamp: models.DBTimestamp{}, + Id: TestUUID, + Name: jobName, + Definition: scheduleIntervalDefModel, + Actions: []models.ScheduleAction{scheduleActionEdgeXMessageBusModel}, + AdminState: models.AdminState(testAdminState), + } +) + +func TestScheduleJob_Validate(t *testing.T) { + validScheduleJob := scheduleJob + invalidId := scheduleJob + invalidId.Id = "123" + emptyName := scheduleJob + emptyName.Name = "" + emptyDef := scheduleJob + emptyDef.Definition = ScheduleDef{} + invalidIntervalDef := scheduleJob + invalidIntervalDef.Definition = ScheduleDef{ + Type: common.DefInterval, + IntervalScheduleDef: IntervalScheduleDef{ + Interval: "", + }, + } + invalidCronDef := scheduleJob + invalidCronDef.Definition = ScheduleDef{ + Type: common.DefCron, + CronScheduleDef: CronScheduleDef{ + Crontab: "", + }, + } + emptyActions := scheduleJob + emptyActions.Actions = nil + invalidEdgeXMessageBusAction := scheduleJob + invalidEdgeXMessageBusAction.Actions = []ScheduleAction{ + { + Type: common.ActionEdgeXMessageBus, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + }, + } + invalidRestAction := scheduleJob + invalidRestAction.Actions = []ScheduleAction{ + { + Type: common.ActionREST, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + }, + } + invalidDeviceControlAction := scheduleJob + invalidDeviceControlAction.Actions = []ScheduleAction{ + { + Type: common.ActionDeviceControl, + ContentType: common.ContentTypeJSON, + Payload: []byte(payload), + }, + } + invalidAdminState := scheduleJob + invalidAdminState.AdminState = "xxx" + + tests := []struct { + name string + request ScheduleJob + expectedErr bool + }{ + {"valid ScheduleJob", validScheduleJob, false}, + {"invalid ScheduleJob, invalid ID", invalidId, true}, + {"invalid ScheduleJob, empty Name", emptyName, true}, + {"invalid ScheduleJob, empty Definition", emptyDef, true}, + {"invalid ScheduleJob, invalid Interval Definition", invalidIntervalDef, true}, + {"invalid ScheduleJob, invalid Cron Definition", invalidCronDef, true}, + {"invalid ScheduleJob, empty Actions", emptyActions, true}, + {"invalid ScheduleJob, invalid EdgeXMessageBus Actions", invalidEdgeXMessageBusAction, true}, + {"invalid ScheduleJob, invalid REST Actions", invalidRestAction, true}, + {"invalid ScheduleJob, invalid DeviceControl Actions", invalidDeviceControlAction, true}, + {"invalid ScheduleJob, invalid AdminState", invalidAdminState, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.request.Validate() + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestToScheduleJobModel(t *testing.T) { + result := ToScheduleJobModel(scheduleJob) + assert.Equal(t, scheduleJobModel, result, "ToScheduleJobModel did not result in ScheduleJob model") +} + +func TestFromScheduleJobModelToDTO(t *testing.T) { + result := FromScheduleJobModelToDTO(scheduleJobModel) + assert.Equal(t, scheduleJob, result, "FromScheduleJobModelToDTO did not result in ScheduleJob dto") +} + +func TestToScheduleDefModel(t *testing.T) { + result := ToScheduleDefModel(scheduleIntervalDef) + assert.Equal(t, scheduleIntervalDefModel, result, "ToScheduleDefModel did not result in Interval ScheduleDef model") + + result2 := ToScheduleDefModel(scheduleCronDef) + assert.Equal(t, scheduleCronDefModel, result2, "ToScheduleDefModel did not result in Cron ScheduleDef model") +} + +func TestFromScheduleDefModelToDTO(t *testing.T) { + result := FromScheduleDefModelToDTO(scheduleIntervalDefModel) + assert.Equal(t, scheduleIntervalDef, result, "FromScheduleDefModelToDTO did not result in Interval ScheduleDef dto") + + result2 := FromScheduleDefModelToDTO(scheduleCronDefModel) + assert.Equal(t, scheduleCronDef, result2, "FromScheduleDefModelToDTO did not result in Cron ScheduleDef dto") +} + +func TestToScheduleActionModel(t *testing.T) { + result := ToScheduleActionModel(scheduleActionEdgeXMessageBus) + assert.Equal(t, scheduleActionEdgeXMessageBusModel, result, "ToScheduleActionModel did not result in EdgeXMessageBus ScheduleAction model") + + result2 := ToScheduleActionModel(scheduleActionRest) + assert.Equal(t, scheduleActionRestModel, result2, "ToScheduleActionModel did not result in REST ScheduleAction model") + + result3 := ToScheduleActionModel(scheduleActionDeviceControl) + assert.Equal(t, scheduleActionDeviceControlModel, result3, "ToScheduleActionModel did not result in DeviceControl ScheduleAction model") +} + +func TestFromScheduleActionModelToDTO(t *testing.T) { + result := FromScheduleActionModelToDTO(scheduleActionEdgeXMessageBusModel) + assert.Equal(t, scheduleActionEdgeXMessageBus, result, "FromScheduleActionModelToDTO did not result in EdgeXMessageBus ScheduleAction dto") + + result2 := FromScheduleActionModelToDTO(scheduleActionRestModel) + assert.Equal(t, scheduleActionRest, result2, "FromScheduleActionModelToDTO did not result in REST ScheduleAction dto") + + result3 := FromScheduleActionModelToDTO(scheduleActionDeviceControlModel) + assert.Equal(t, scheduleActionDeviceControl, result3, "FromScheduleActionModelToDTO did not result in DeviceControl ScheduleAction dto") +} diff --git a/models/const_test.go b/models/const_test.go index 21de9ded..8e5fd890 100644 --- a/models/const_test.go +++ b/models/const_test.go @@ -8,6 +8,7 @@ package models const ( ExampleUUID = "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" TestIntervalName = "TestInterval" + TestInterval = "10m" TestIntervalActionName = "TestIntervalAction" TestHost = "localhost" TestPort = 48089 @@ -17,4 +18,13 @@ const ( TestSubscriptionName = "TestSubscriptionName" TestSubscriptionReceiver = "TestReceiver" + + TestScheduleJobName = "TestScheduleJob" + TestCrontab = "0 0 1 1 *" + TestContentType = "application/json" + TestPayload = "eyJ0ZXN0I" + TestAddress = "http://localhost:12345/test/address" + TestDeviceName = "TestDeviceName" + TestSourceName = "TestSourceName" + TestScheduleActionRecordStatus = "SUCCEED" ) diff --git a/models/consts.go b/models/consts.go index e1bfe259..1ced58ed 100644 --- a/models/consts.go +++ b/models/consts.go @@ -31,12 +31,16 @@ const ( EscalatedContentNotice = "This notification is escalated by the transmission" ) -// Constants for TransmissionStatus +// Constants for TransmissionStatus and ScheduleActionRecordStatus const ( Failed = "FAILED" Sent = "SENT" Acknowledged = "ACKNOWLEDGED" RESENDING = "RESENDING" + + // Constants for ScheduleActionRecordStatus only + Succeeded = "SUCCEEDED" + Missed = "MISSED" ) // Constants for both NotificationStatus and TransmissionStatus diff --git a/models/scheduleactionrecord.go b/models/scheduleactionrecord.go new file mode 100644 index 00000000..9a12dc85 --- /dev/null +++ b/models/scheduleactionrecord.go @@ -0,0 +1,54 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package models + +import ( + "encoding/json" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +type ScheduleActionRecord struct { + Id string + JobName string + Action ScheduleAction + Status ScheduleActionRecordStatus + ScheduledAt int64 + Created int64 +} + +// ScheduleActionRecordStatus indicates the most recent success/failure of a given schedule action attempt or a missed record. +type ScheduleActionRecordStatus string + +func (scheduleActionRecord *ScheduleActionRecord) UnmarshalJSON(b []byte) error { + var alias struct { + Id string + JobName string + Action any + Status ScheduleActionRecordStatus + ScheduledAt int64 + Created int64 + } + + if err := json.Unmarshal(b, &alias); err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal ScheduleActionRecord.", err) + } + + action, err := instantiateScheduleAction(alias.Action) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + + *scheduleActionRecord = ScheduleActionRecord{ + Id: alias.Id, + JobName: alias.JobName, + Action: action, + Status: alias.Status, + ScheduledAt: alias.ScheduledAt, + Created: alias.Created, + } + return nil +} diff --git a/models/scheduleactionrecord_test.go b/models/scheduleactionrecord_test.go new file mode 100644 index 00000000..3b83f142 --- /dev/null +++ b/models/scheduleactionrecord_test.go @@ -0,0 +1,84 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package models + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func scheduleActionRecordWithEDGEXMESSAGEBUSScheduleAction() ScheduleActionRecord { + return ScheduleActionRecord{ + Id: ExampleUUID, + JobName: TestScheduleJobName, + Action: edgeXMessageBusAction, + } +} + +func scheduleActionRecordWithRESTScheduleAction() ScheduleActionRecord { + return ScheduleActionRecord{ + Id: ExampleUUID, + JobName: TestScheduleJobName, + Action: restAction, + } +} + +func scheduleActionRecordWithDEVICECONTROLScheduleAction() ScheduleActionRecord { + return ScheduleActionRecord{ + Id: ExampleUUID, + JobName: TestScheduleJobName, + Action: deviceControlAction, + } +} + +func TestScheduleActionRecord_UnmarshalJSON(t *testing.T) { + scheduleActionRecordWithEdgeXMessageBusScheduleAction := scheduleActionRecordWithEDGEXMESSAGEBUSScheduleAction() + scheduleActionRecordWithEdgeXMessageBusScheduleActionJsonData, err := json.Marshal(scheduleActionRecordWithEdgeXMessageBusScheduleAction) + require.NoError(t, err) + + scheduleActionRecordWithRestScheduleAction := scheduleActionRecordWithRESTScheduleAction() + scheduleActionRecordWithRestScheduleActionJsonData, err := json.Marshal(scheduleActionRecordWithRestScheduleAction) + require.NoError(t, err) + + scheduleActionRecordWithDeviceControlScheduleAction := scheduleActionRecordWithDEVICECONTROLScheduleAction() + scheduleActionRecordWithDeviceControlScheduleActionJsonData, err := json.Marshal(scheduleActionRecordWithDeviceControlScheduleAction) + require.NoError(t, err) + + scheduleActionRecordWithInvalidScheduleAction := scheduleActionRecordWithDEVICECONTROLScheduleAction() + scheduleActionRecordWithInvalidScheduleAction.Action = nil + scheduleActionRecordWithInvalidScheduleActionJsonData, err := json.Marshal(scheduleActionRecordWithInvalidScheduleAction) + require.NoError(t, err) + + tests := []struct { + name string + expected ScheduleActionRecord + data []byte + wantErr bool + }{ + {"valid, unmarshal ScheduleActionRecord with EDGEXMESSAGEBUS ScheduleAction", scheduleActionRecordWithEdgeXMessageBusScheduleAction, scheduleActionRecordWithEdgeXMessageBusScheduleActionJsonData, false}, + {"valid, unmarshal ScheduleActionRecord with REST ScheduleAction", scheduleActionRecordWithRestScheduleAction, scheduleActionRecordWithRestScheduleActionJsonData, false}, + {"valid, unmarshal ScheduleActionRecord with DEVICECONTROL ScheduleAction", scheduleActionRecordWithDeviceControlScheduleAction, scheduleActionRecordWithDeviceControlScheduleActionJsonData, false}, + {"unmarshal ScheduleActionRecord with invalid ScheduleAction", scheduleActionRecordWithInvalidScheduleAction, scheduleActionRecordWithInvalidScheduleActionJsonData, true}, + {"unmarshal invalid ScheduleActionRecord, invalid data", ScheduleActionRecord{}, []byte(`{"Created": [1]}`), true}, + {"unmarshal invalid ScheduleActionRecord, empty data", ScheduleActionRecord{}, []byte{}, true}, + {"unmarshal invalid ScheduleActionRecord, string data", ScheduleActionRecord{}, []byte("Invalid ScheduleActionRecord"), true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result ScheduleActionRecord + err := json.Unmarshal(tt.data, &result) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result, "Unmarshal did not result in expected ScheduleActionRecord.") + } + }) + } +} diff --git a/models/schedulejob.go b/models/schedulejob.go new file mode 100644 index 00000000..cce26560 --- /dev/null +++ b/models/schedulejob.go @@ -0,0 +1,230 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package models + +import ( + "encoding/json" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/errors" +) + +type ScheduleJob struct { + DBTimestamp + Id string + Name string + Definition ScheduleDef + Actions []ScheduleAction + AdminState AdminState + Labels []string + Properties map[string]any +} + +func (scheduleJob *ScheduleJob) UnmarshalJSON(b []byte) error { + var alias struct { + DBTimestamp + Id string + Name string + Definition any + Actions []any + AdminState AdminState + Labels []string + Properties map[string]any + } + + if err := json.Unmarshal(b, &alias); err != nil { + return errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal ScheduleJob.", err) + } + + def, err := instantiateScheduleDef(alias.Definition) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + + actions := make([]ScheduleAction, len(alias.Actions)) + for i, a := range alias.Actions { + action, err := instantiateScheduleAction(a) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + actions[i] = action + } + + *scheduleJob = ScheduleJob{ + DBTimestamp: alias.DBTimestamp, + Id: alias.Id, + Name: alias.Name, + Definition: def, + Actions: actions, + AdminState: alias.AdminState, + Labels: alias.Labels, + Properties: alias.Properties, + } + return nil +} + +type ScheduleDef interface { + GetBaseScheduleDef() BaseScheduleDef +} + +// instantiateScheduleDef instantiate the interface to the corresponding schedule definition type +func instantiateScheduleDef(i any) (def ScheduleDef, err error) { + d, err := json.Marshal(i) + if err != nil { + return def, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to marshal ScheduleDef.", err) + } + return unmarshalScheduleDef(d) +} + +func unmarshalScheduleDef(b []byte) (def ScheduleDef, err error) { + var alias struct { + Type string + } + if err = json.Unmarshal(b, &alias); err != nil { + return def, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal ScheduleDef.", err) + } + switch alias.Type { + case common.DefInterval: + var intervalDef IntervalScheduleDef + if err = json.Unmarshal(b, &intervalDef); err != nil { + return def, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal INTERVAL ScheduleDef.", err) + } + def = intervalDef + case common.DefCron: + var cronDef CronScheduleDef + if err = json.Unmarshal(b, &cronDef); err != nil { + return def, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal CRON ScheduleDef.", err) + } + def = cronDef + default: + return def, errors.NewCommonEdgeX(errors.KindContractInvalid, "Unsupported schedule definition type", err) + } + return def, nil +} + +type BaseScheduleDef struct { + Type ScheduleDefType +} + +type IntervalScheduleDef struct { + BaseScheduleDef + // Interval specifies the time interval between two consecutive executions + Interval string +} + +func (d IntervalScheduleDef) GetBaseScheduleDef() BaseScheduleDef { + return d.BaseScheduleDef +} + +type CronScheduleDef struct { + BaseScheduleDef + // Crontab is the cron expression + Crontab string +} + +func (c CronScheduleDef) GetBaseScheduleDef() BaseScheduleDef { + return c.BaseScheduleDef +} + +type ScheduleAction interface { + GetBaseScheduleAction() BaseScheduleAction + // WithEmptyPayload returns a copy of the ScheduleAction with empty payload, which is used by ScheduleActionRecord to remove the payload before storing the record into database + WithEmptyPayload() ScheduleAction +} + +// instantiateScheduleAction instantiate the interface to the corresponding schedule action type +func instantiateScheduleAction(i any) (action ScheduleAction, err error) { + a, err := json.Marshal(i) + if err != nil { + return action, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to marshal ScheduleAction.", err) + } + return unmarshalScheduleAction(a) +} + +func unmarshalScheduleAction(b []byte) (action ScheduleAction, err error) { + var alias struct { + Type string + } + if err = json.Unmarshal(b, &alias); err != nil { + return action, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal ScheduleAction.", err) + } + switch alias.Type { + case common.ActionEdgeXMessageBus: + var edgeXMessageBusAction EdgeXMessageBusAction + if err = json.Unmarshal(b, &edgeXMessageBusAction); err != nil { + return action, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal EDGEXMESSAGEBUS ScheduleAction.", err) + } + action = edgeXMessageBusAction + case common.ActionREST: + var restAction RESTAction + if err = json.Unmarshal(b, &restAction); err != nil { + return action, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal REST ScheduleAction.", err) + } + action = restAction + case common.ActionDeviceControl: + var deviceControlAction DeviceControlAction + if err = json.Unmarshal(b, &deviceControlAction); err != nil { + return action, errors.NewCommonEdgeX(errors.KindContractInvalid, "Failed to unmarshal DEVICECONTROL ScheduleAction.", err) + } + action = deviceControlAction + default: + return action, errors.NewCommonEdgeX(errors.KindContractInvalid, "Unsupported schedule action type", err) + } + return action, nil +} + +type BaseScheduleAction struct { + Type ScheduleActionType + ContentType string + Payload []byte +} + +type EdgeXMessageBusAction struct { + BaseScheduleAction + Topic string +} + +func (m EdgeXMessageBusAction) GetBaseScheduleAction() BaseScheduleAction { + return m.BaseScheduleAction +} +func (m EdgeXMessageBusAction) WithEmptyPayload() ScheduleAction { + m.Payload = nil + return m +} + +type RESTAction struct { + BaseScheduleAction + Address string + InjectEdgeXAuth bool +} + +func (r RESTAction) GetBaseScheduleAction() BaseScheduleAction { + return r.BaseScheduleAction +} +func (r RESTAction) WithEmptyPayload() ScheduleAction { + r.Payload = nil + return r +} + +type DeviceControlAction struct { + BaseScheduleAction + DeviceName string + SourceName string +} + +func (d DeviceControlAction) GetBaseScheduleAction() BaseScheduleAction { + return d.BaseScheduleAction +} +func (d DeviceControlAction) WithEmptyPayload() ScheduleAction { + d.Payload = nil + return d +} + +// ScheduleDefType is used to identify the schedule definition type, i.e., INTERVAL or CRON +type ScheduleDefType string + +// ScheduleActionType is used to identify the schedule action type, i.e., EDGEXMESSAGEBUS, REST, or DEVICECONTROL +type ScheduleActionType string diff --git a/models/schedulejob_test.go b/models/schedulejob_test.go new file mode 100644 index 00000000..c3fb9785 --- /dev/null +++ b/models/schedulejob_test.go @@ -0,0 +1,316 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package models + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/common" +) + +var intervalScheduleDef = IntervalScheduleDef{ + BaseScheduleDef: BaseScheduleDef{ + Type: common.DefInterval, + }, + Interval: TestInterval, +} + +var cronScheduleDef = CronScheduleDef{ + BaseScheduleDef: BaseScheduleDef{ + Type: common.DefCron, + }, + Crontab: TestCrontab, +} + +var scheduleJobWithInvalidIntervalScheduleDef = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "INTERVAL", + "Interval": ["123"] + }, + "actions": [] +}` + +var scheduleJobWithInvalidCronScheduleDef = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "CRON", + "Crontab": ["123"] + }, + "actions": [] +}` + +var scheduleJobWithUnsupportedScheduleDef = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "NOT_SUPPORTED", + "Interval": "10m" + }, + "actions": [] +}` + +var scheduleJobWithInvalidScheduleDef = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": [], + "actions": [] +}` + +var scheduleJobWithInvalidEdgeXMessageBusAction = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "INTERVAL", + "Interval": "10m" + }, + "actions": [ + { + "type": "EDGEXMESSAGEBUS", + "contentType": "application/json", + "payload": "eyJ0ZXN0I", + "typo": "testTopic" + } + ] +}` + +var scheduleJobWithInvalidRestAction = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "INTERVAL", + "Interval": "10m" + }, + "actions": [ + { + "type": "REST", + "contentType": "application/json", + "payload": "eyJ0ZXN0I", + "address": ["http://localhost:12345/test/address"] + } + ] +}` + +var scheduleJobWithInvalidDeviceControlAction = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "INTERVAL", + "Interval": "10m" + }, + "actions": [ + { + "type": "DEVICECONTROL", + "contentType": "application/json", + "payload": "eyJ0ZXN0I", + "deviceName": ["123"], + "typoName": "testSourceName" + } + ] +}` + +var scheduleJobWithUnsupportedAction = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "INTERVAL", + "Interval": "10m" + }, + "actions": [ + { + "type": "UNSUPPORTED" + } + ] +}` + +var scheduleJobWithInvalidScheduleAction = `{ + "id": "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc", + "name": "TestScheduleJob", + "definition": { + "Type": "INTERVAL", + "Interval": "10m" + }, + "actions": ["123"] +}` + +var edgeXMessageBusAction = EdgeXMessageBusAction{ + BaseScheduleAction: BaseScheduleAction{ + Type: common.ActionEdgeXMessageBus, + ContentType: TestContentType, + Payload: []byte(TestPayload), + }, + Topic: TestTopic, +} + +var restAction = RESTAction{ + BaseScheduleAction: BaseScheduleAction{ + Type: common.ActionREST, + ContentType: TestContentType, + Payload: []byte(TestPayload), + }, + Address: TestAddress, +} + +var deviceControlAction = DeviceControlAction{ + BaseScheduleAction: BaseScheduleAction{ + Type: common.ActionDeviceControl, + ContentType: TestContentType, + Payload: []byte(TestPayload), + }, + DeviceName: TestDeviceName, + SourceName: TestSourceName, +} + +func scheduleJobWithINTERVALScheduleDef() ScheduleJob { + return ScheduleJob{ + DBTimestamp: DBTimestamp{}, + Id: ExampleUUID, + Name: TestScheduleJobName, + Definition: intervalScheduleDef, + Actions: []ScheduleAction{}, + } +} + +func scheduleJobWithCRONScheduleDef() ScheduleJob { + return ScheduleJob{ + DBTimestamp: DBTimestamp{}, + Id: ExampleUUID, + Name: TestScheduleJobName, + Definition: cronScheduleDef, + Actions: []ScheduleAction{}, + } +} + +func scheduleJobWithEDGEXMESSAGEBUSScheduleAction() ScheduleJob { + return ScheduleJob{ + DBTimestamp: DBTimestamp{}, + Id: ExampleUUID, + Name: TestScheduleJobName, + Definition: intervalScheduleDef, + Actions: []ScheduleAction{edgeXMessageBusAction}, + } +} + +func scheduleJobWithRESTScheduleAction() ScheduleJob { + return ScheduleJob{ + DBTimestamp: DBTimestamp{}, + Id: ExampleUUID, + Name: TestScheduleJobName, + Definition: intervalScheduleDef, + Actions: []ScheduleAction{restAction}, + } +} + +func scheduleJobWithDEVICECONTROLScheduleAction() ScheduleJob { + return ScheduleJob{ + DBTimestamp: DBTimestamp{}, + Id: ExampleUUID, + Name: TestScheduleJobName, + Definition: intervalScheduleDef, + Actions: []ScheduleAction{deviceControlAction}, + } +} + +func TestScheduleJob_UnmarshalJSON(t *testing.T) { + scheduleJobWithIntervalScheduleDef := scheduleJobWithINTERVALScheduleDef() + scheduleJobWithIntervalScheduleDefJsonData, err := json.Marshal(scheduleJobWithIntervalScheduleDef) + require.NoError(t, err) + + scheduleJobWithCronScheduleDef := scheduleJobWithCRONScheduleDef() + scheduleJobWithCronScheduleDefJsonData, err := json.Marshal(scheduleJobWithCronScheduleDef) + require.NoError(t, err) + + scheduleJobWithEdgeXMessageBusScheduleAction := scheduleJobWithEDGEXMESSAGEBUSScheduleAction() + scheduleJobWithEdgeXMessageBusScheduleActionJsonData, err := json.Marshal(scheduleJobWithEdgeXMessageBusScheduleAction) + require.NoError(t, err) + + scheduleJobWithRestScheduleAction := scheduleJobWithRESTScheduleAction() + scheduleJobWithRestScheduleActionJsonData, err := json.Marshal(scheduleJobWithRestScheduleAction) + require.NoError(t, err) + + scheduleJobWithDeviceControlScheduleAction := scheduleJobWithDEVICECONTROLScheduleAction() + scheduleJobWithDeviceControlScheduleActionJsonData, err := json.Marshal(scheduleJobWithDeviceControlScheduleAction) + require.NoError(t, err) + + tests := []struct { + name string + expected ScheduleJob + data []byte + wantErr bool + }{ + {"valid, unmarshal ScheduleJob with INTERVAL ScheduleDef", scheduleJobWithIntervalScheduleDef, scheduleJobWithIntervalScheduleDefJsonData, false}, + {"unmarshal ScheduleJob with invalid INTERVAL ScheduleDef", ScheduleJob{}, []byte(scheduleJobWithInvalidIntervalScheduleDef), true}, + {"valid, unmarshal ScheduleJob with CRON ScheduleDef", scheduleJobWithCronScheduleDef, scheduleJobWithCronScheduleDefJsonData, false}, + {"unmarshal ScheduleJob with invalid CRON ScheduleDef", scheduleJobWithCronScheduleDef, []byte(scheduleJobWithInvalidCronScheduleDef), true}, + {"unmarshal ScheduleJob with unsupported ScheduleDef", ScheduleJob{}, []byte(scheduleJobWithUnsupportedScheduleDef), true}, + {"unmarshal ScheduleJob with invalid ScheduleDef", ScheduleJob{}, []byte(scheduleJobWithInvalidScheduleDef), true}, + {"valid, unmarshal ScheduleJob with EDGEXMESSAGEBUS ScheduleAction", scheduleJobWithEdgeXMessageBusScheduleAction, scheduleJobWithEdgeXMessageBusScheduleActionJsonData, false}, + {"unmarshal ScheduleJob with invalid EDGEXMESSAGEBUS ScheduleAction", ScheduleJob{}, []byte(scheduleJobWithInvalidEdgeXMessageBusAction), true}, + {"valid, unmarshal ScheduleJob with REST ScheduleAction", scheduleJobWithRestScheduleAction, scheduleJobWithRestScheduleActionJsonData, false}, + {"unmarshal ScheduleJob with invalid REST ScheduleAction", ScheduleJob{}, []byte(scheduleJobWithInvalidRestAction), true}, + {"valid, unmarshal ScheduleJob with DEVICECONTROL ScheduleAction", scheduleJobWithDeviceControlScheduleAction, scheduleJobWithDeviceControlScheduleActionJsonData, false}, + {"unmarshal ScheduleJob with invalid DEVICECONTROL ScheduleAction", ScheduleJob{}, []byte(scheduleJobWithInvalidDeviceControlAction), true}, + {"unmarshal ScheduleJob with unsupported ScheduleAction", ScheduleJob{}, []byte(scheduleJobWithUnsupportedAction), true}, + {"unmarshal ScheduleJob with invalid ScheduleAction", ScheduleJob{}, []byte(scheduleJobWithInvalidScheduleAction), true}, + {"unmarshal invalid ScheduleJob, invalid data", ScheduleJob{}, []byte(`{"Created": [1]}`), true}, + {"unmarshal invalid ScheduleJob, empty data", ScheduleJob{}, []byte{}, true}, + {"unmarshal invalid ScheduleJob, string data", ScheduleJob{}, []byte("Invalid ScheduleJob"), true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result ScheduleJob + err := json.Unmarshal(tt.data, &result) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result, "Unmarshal did not result in expected ScheduleJob.") + } + }) + } +} + +func TestScheduleAction_GetBaseScheduleAction(t *testing.T) { + tests := []struct { + name string + action ScheduleAction + expected BaseScheduleAction + }{ + {"EdgeXMessageBusAction", edgeXMessageBusAction, edgeXMessageBusAction.BaseScheduleAction}, + {"RESTAction", restAction, restAction.BaseScheduleAction}, + {"DeviceControlAction", deviceControlAction, deviceControlAction.BaseScheduleAction}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.action.GetBaseScheduleAction() + assert.Equal(t, tt.expected, result, "GetBaseScheduleAction did not result in expected BaseScheduleAction.") + }) + } +} + +func TestScheduleAction_WithEmptyPayload(t *testing.T) { + tests := []struct { + name string + action ScheduleAction + expected ScheduleAction + }{ + {"EdgeXMessageBusAction", edgeXMessageBusAction, edgeXMessageBusAction.WithEmptyPayload()}, + {"RESTAction", restAction, restAction.WithEmptyPayload()}, + {"DeviceControlAction", deviceControlAction, deviceControlAction.WithEmptyPayload()}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.action.WithEmptyPayload() + assert.Nil(t, result.GetBaseScheduleAction().Payload, "WithEmptyPayload did not result in empty Payload.") + }) + } +}