From 132cf7a0d98f69aaa4a72c8537e6ff006b27445a Mon Sep 17 00:00:00 2001 From: "Corey Mutter @ Eaton" <89539499+eaton-coreymutter@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:50:50 -0400 Subject: [PATCH] feat: Add optional "parent" field to Device objects. (#887) * feat: Add optional Parent field to device objects. Also add DeviceClient function to query a tree of devices. Signed-off-by: Corey Mutter * feat: Add Parent field (revert change to timestamp propagation) Signed-off-by: Corey Mutter * fix: Formatting. Signed-off-by: Corey Mutter * feat: Add Parent field (update constant name, add maxLevels to query) Signed-off-by: Corey Mutter --------- Signed-off-by: Corey Mutter --- clients/http/device.go | 16 ++++ clients/http/device_test.go | 9 ++ clients/interfaces/device.go | 7 ++ clients/interfaces/mocks/DeviceClient.go | 113 ++++++++++++++++++++--- common/constants.go | 22 +++-- dtos/device.go | 5 + dtos/device_test.go | 90 +++++++++++++++--- dtos/requests/device.go | 3 + dtos/requests/device_test.go | 14 ++- models/device.go | 1 + 10 files changed, 243 insertions(+), 37 deletions(-) diff --git a/clients/http/device.go b/clients/http/device.go index 09895737..3a2deed9 100644 --- a/clients/http/device.go +++ b/clients/http/device.go @@ -66,6 +66,22 @@ func (dc DeviceClient) AllDevices(ctx context.Context, labels []string, offset i return res, nil } +func (dc DeviceClient) AllDevicesWithChildren(ctx context.Context, parent string, maxLevels uint, labels []string, offset int, limit int) (res responses.MultiDevicesResponse, err errors.EdgeX) { + requestParams := url.Values{} + if len(labels) > 0 { + requestParams.Set(common.Labels, strings.Join(labels, common.CommaSeparator)) + } + requestParams.Set(common.DescendantsOf, parent) + requestParams.Set(common.MaxLevels, strconv.FormatUint(uint64(maxLevels), 10)) + requestParams.Set(common.Offset, strconv.Itoa(offset)) + requestParams.Set(common.Limit, strconv.Itoa(limit)) + err = utils.GetRequest(ctx, &res, dc.baseUrl, common.ApiAllDeviceRoute, requestParams, dc.authInjector) + if err != nil { + return res, errors.NewCommonEdgeXWrapper(err) + } + return res, nil +} + func (dc DeviceClient) DeviceNameExists(ctx context.Context, name string) (res dtoCommon.BaseResponse, err errors.EdgeX) { path := common.NewPathBuilder().EnableNameFieldEscape(dc.enableNameFieldEscape). SetPath(common.ApiDeviceRoute).SetPath(common.Check).SetPath(common.Name).SetNameFieldPath(name).BuildPath() diff --git a/clients/http/device_test.go b/clients/http/device_test.go index 73395831..349cd4e9 100644 --- a/clients/http/device_test.go +++ b/clients/http/device_test.go @@ -101,3 +101,12 @@ func TestQueryDevicesByServiceName(t *testing.T) { require.NoError(t, err) require.IsType(t, responses.MultiDevicesResponse{}, res) } + +func TestQueryDeviceTree(t *testing.T) { + ts := newTestServer(http.MethodGet, common.ApiAllDeviceRoute, responses.MultiDevicesResponse{}) + defer ts.Close() + client := NewDeviceClient(ts.URL, NewNullAuthenticationInjector(), false) + res, err := client.AllDevicesWithChildren(context.Background(), "MyRoot", 3, []string{"label1", "label2"}, 1, 10) + require.NoError(t, err) + require.IsType(t, responses.MultiDevicesResponse{}, res) +} diff --git a/clients/interfaces/device.go b/clients/interfaces/device.go index b277c660..f1114b85 100644 --- a/clients/interfaces/device.go +++ b/clients/interfaces/device.go @@ -25,6 +25,13 @@ type DeviceClient interface { // 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. AllDevices(ctx context.Context, labels []string, offset int, limit int) (responses.MultiDevicesResponse, errors.EdgeX) + // AllDevicesWithChildren returns all devices who have parent, grandparent, etc. of the + // given device name. Devices can also be filtered by labels. + // Device tree is descended at most maxLevels. If maxLevels is 0, there is no limit. + // 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. + AllDevicesWithChildren(ctx context.Context, parent string, maxLevels uint, labels []string, offset int, limit int) (responses.MultiDevicesResponse, errors.EdgeX) // DeviceNameExists checks whether the device exists. DeviceNameExists(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) // DeviceByName returns a device by device name. diff --git a/clients/interfaces/mocks/DeviceClient.go b/clients/interfaces/mocks/DeviceClient.go index 3b1af7fe..09379d20 100644 --- a/clients/interfaces/mocks/DeviceClient.go +++ b/clients/interfaces/mocks/DeviceClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.42.1. DO NOT EDIT. package mocks @@ -25,7 +25,15 @@ type DeviceClient struct { func (_m *DeviceClient) Add(ctx context.Context, reqs []requests.AddDeviceRequest) ([]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.AddDeviceRequest) ([]common.BaseWithIdResponse, errors.EdgeX)); ok { + return rf(ctx, reqs) + } if rf, ok := ret.Get(0).(func(context.Context, []requests.AddDeviceRequest) []common.BaseWithIdResponse); ok { r0 = rf(ctx, reqs) } else { @@ -34,7 +42,6 @@ func (_m *DeviceClient) Add(ctx context.Context, reqs []requests.AddDeviceReques } } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, []requests.AddDeviceRequest) errors.EdgeX); ok { r1 = rf(ctx, reqs) } else { @@ -50,14 +57,21 @@ func (_m *DeviceClient) Add(ctx context.Context, reqs []requests.AddDeviceReques func (_m *DeviceClient) AllDevices(ctx context.Context, labels []string, offset int, limit int) (responses.MultiDevicesResponse, errors.EdgeX) { ret := _m.Called(ctx, labels, offset, limit) + if len(ret) == 0 { + panic("no return value specified for AllDevices") + } + var r0 responses.MultiDevicesResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, []string, int, int) (responses.MultiDevicesResponse, errors.EdgeX)); ok { + return rf(ctx, labels, offset, limit) + } if rf, ok := ret.Get(0).(func(context.Context, []string, int, int) responses.MultiDevicesResponse); ok { r0 = rf(ctx, labels, offset, limit) } else { r0 = ret.Get(0).(responses.MultiDevicesResponse) } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, []string, int, int) errors.EdgeX); ok { r1 = rf(ctx, labels, offset, limit) } else { @@ -69,18 +83,55 @@ func (_m *DeviceClient) AllDevices(ctx context.Context, labels []string, offset return r0, r1 } +// AllDevicesWithChildren provides a mock function with given fields: ctx, parent, labels, offset, limit +func (_m *DeviceClient) AllDevicesWithChildren(ctx context.Context, parent string, labels []string, offset int, limit int) (responses.MultiDevicesResponse, errors.EdgeX) { + ret := _m.Called(ctx, parent, labels, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for AllDevicesWithChildren") + } + + var r0 responses.MultiDevicesResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string, []string, int, int) (responses.MultiDevicesResponse, errors.EdgeX)); ok { + return rf(ctx, parent, labels, offset, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []string, int, int) responses.MultiDevicesResponse); ok { + r0 = rf(ctx, parent, labels, offset, limit) + } else { + r0 = ret.Get(0).(responses.MultiDevicesResponse) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []string, int, int) errors.EdgeX); ok { + r1 = rf(ctx, parent, labels, offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + // DeleteDeviceByName provides a mock function with given fields: ctx, name func (_m *DeviceClient) DeleteDeviceByName(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for DeleteDeviceByName") + } + 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) } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { r1 = rf(ctx, name) } else { @@ -96,14 +147,21 @@ func (_m *DeviceClient) DeleteDeviceByName(ctx context.Context, name string) (co func (_m *DeviceClient) DeviceByName(ctx context.Context, name string) (responses.DeviceResponse, errors.EdgeX) { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for DeviceByName") + } + var r0 responses.DeviceResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string) (responses.DeviceResponse, errors.EdgeX)); ok { + return rf(ctx, name) + } if rf, ok := ret.Get(0).(func(context.Context, string) responses.DeviceResponse); ok { r0 = rf(ctx, name) } else { r0 = ret.Get(0).(responses.DeviceResponse) } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { r1 = rf(ctx, name) } else { @@ -119,14 +177,21 @@ func (_m *DeviceClient) DeviceByName(ctx context.Context, name string) (response func (_m *DeviceClient) DeviceNameExists(ctx context.Context, name string) (common.BaseResponse, errors.EdgeX) { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for DeviceNameExists") + } + 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) } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, string) errors.EdgeX); ok { r1 = rf(ctx, name) } else { @@ -142,14 +207,21 @@ func (_m *DeviceClient) DeviceNameExists(ctx context.Context, name string) (comm func (_m *DeviceClient) DevicesByProfileName(ctx context.Context, name string, offset int, limit int) (responses.MultiDevicesResponse, errors.EdgeX) { ret := _m.Called(ctx, name, offset, limit) + if len(ret) == 0 { + panic("no return value specified for DevicesByProfileName") + } + var r0 responses.MultiDevicesResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string, int, int) (responses.MultiDevicesResponse, errors.EdgeX)); ok { + return rf(ctx, name, offset, limit) + } if rf, ok := ret.Get(0).(func(context.Context, string, int, int) responses.MultiDevicesResponse); ok { r0 = rf(ctx, name, offset, limit) } else { r0 = ret.Get(0).(responses.MultiDevicesResponse) } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, string, int, int) errors.EdgeX); ok { r1 = rf(ctx, name, offset, limit) } else { @@ -165,14 +237,21 @@ func (_m *DeviceClient) DevicesByProfileName(ctx context.Context, name string, o func (_m *DeviceClient) DevicesByServiceName(ctx context.Context, name string, offset int, limit int) (responses.MultiDevicesResponse, errors.EdgeX) { ret := _m.Called(ctx, name, offset, limit) + if len(ret) == 0 { + panic("no return value specified for DevicesByServiceName") + } + var r0 responses.MultiDevicesResponse + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(context.Context, string, int, int) (responses.MultiDevicesResponse, errors.EdgeX)); ok { + return rf(ctx, name, offset, limit) + } if rf, ok := ret.Get(0).(func(context.Context, string, int, int) responses.MultiDevicesResponse); ok { r0 = rf(ctx, name, offset, limit) } else { r0 = ret.Get(0).(responses.MultiDevicesResponse) } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, string, int, int) errors.EdgeX); ok { r1 = rf(ctx, name, offset, limit) } else { @@ -188,7 +267,15 @@ func (_m *DeviceClient) DevicesByServiceName(ctx context.Context, name string, o func (_m *DeviceClient) Update(ctx context.Context, reqs []requests.UpdateDeviceRequest) ([]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.UpdateDeviceRequest) ([]common.BaseResponse, errors.EdgeX)); ok { + return rf(ctx, reqs) + } if rf, ok := ret.Get(0).(func(context.Context, []requests.UpdateDeviceRequest) []common.BaseResponse); ok { r0 = rf(ctx, reqs) } else { @@ -197,7 +284,6 @@ func (_m *DeviceClient) Update(ctx context.Context, reqs []requests.UpdateDevice } } - var r1 errors.EdgeX if rf, ok := ret.Get(1).(func(context.Context, []requests.UpdateDeviceRequest) errors.EdgeX); ok { r1 = rf(ctx, reqs) } else { @@ -209,13 +295,12 @@ func (_m *DeviceClient) Update(ctx context.Context, reqs []requests.UpdateDevice return r0, r1 } -type mockConstructorTestingTNewDeviceClient interface { +// NewDeviceClient creates a new instance of DeviceClient. 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 NewDeviceClient(t interface { mock.TestingT Cleanup(func()) -} - -// NewDeviceClient creates a new instance of DeviceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDeviceClient(t mockConstructorTestingTNewDeviceClient) *DeviceClient { +}) *DeviceClient { mock := &DeviceClient{} mock.Mock.Test(t) diff --git a/common/constants.go b/common/constants.go index 1288109e..a8623757 100644 --- a/common/constants.go +++ b/common/constants.go @@ -185,16 +185,18 @@ const ( Key = "key" ServiceId = "serviceId" - 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 - Labels = "labels" //query string to specify associated user-defined labels for querying a given object. More than one label may be specified via a comma-delimited list - PushEvent = "ds-pushevent" //query string to specify if an event should be pushed to the EdgeX system - ReturnEvent = "ds-returnevent" //query string to specify if an event should be returned from device service - RegexCommand = "ds-regexcmd" //query string to specify if the command name is in regular expression format - Flatten = "flatten" //query string to specify if the request json payload should be flattened to update multiple keys with the same prefix - KeyOnly = "keyOnly" //query string to specify if the response will only return the keys of the specified query key prefix, without values and metadata - Plaintext = "plaintext" //query string to specify if the response will return the stored plain text value of the key(s) without any encoding - Deregistered = "deregistered" //query string to specify if the response will return the registries of deregistered services + 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 + Labels = "labels" //query string to specify associated user-defined labels for querying a given object. More than one label may be specified via a comma-delimited list + PushEvent = "ds-pushevent" //query string to specify if an event should be pushed to the EdgeX system + ReturnEvent = "ds-returnevent" //query string to specify if an event should be returned from device service + RegexCommand = "ds-regexcmd" //query string to specify if the command name is in regular expression format + DescendantsOf = "descendantsOf" //Limit returned devices to those who have parent, grandparent, etc. of the given device name + MaxLevels = "maxLevels" //Limit returned devices to this many levels below 'descendantsOf' (0=unlimited) + Flatten = "flatten" //query string to specify if the request json payload should be flattened to update multiple keys with the same prefix + KeyOnly = "keyOnly" //query string to specify if the response will only return the keys of the specified query key prefix, without values and metadata + Plaintext = "plaintext" //query string to specify if the response will return the stored plain text value of the key(s) without any encoding + Deregistered = "deregistered" //query string to specify if the response will return the registries of deregistered services ) // Constants related to the default value of query strings in the v3 service APIs diff --git a/dtos/device.go b/dtos/device.go index d7f96e91..a1f34ec4 100644 --- a/dtos/device.go +++ b/dtos/device.go @@ -13,6 +13,7 @@ type Device struct { DBTimestamp `json:",inline"` Id string `json:"id,omitempty" yaml:"id,omitempty" validate:"omitempty,uuid"` Name string `json:"name" yaml:"name" validate:"required,edgex-dto-none-empty-string"` + Parent string `json:"parent,omitempty" yaml:"parent,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` AdminState string `json:"adminState" yaml:"adminState" validate:"oneof='LOCKED' 'UNLOCKED'"` OperatingState string `json:"operatingState" yaml:"operatingState" validate:"oneof='UP' 'DOWN' 'UNKNOWN'"` @@ -29,6 +30,7 @@ type Device struct { type UpdateDevice 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"` + Parent *string `json:"parent,omitempty" yaml:"parent,omitempty"` Description *string `json:"description" validate:"omitempty"` AdminState *string `json:"adminState" validate:"omitempty,oneof='LOCKED' 'UNLOCKED'"` OperatingState *string `json:"operatingState" validate:"omitempty,oneof='UP' 'DOWN' 'UNKNOWN'"` @@ -47,6 +49,7 @@ func ToDeviceModel(dto Device) models.Device { var d models.Device d.Id = dto.Id d.Name = dto.Name + d.Parent = dto.Parent d.Description = dto.Description d.ServiceName = dto.ServiceName d.ProfileName = dto.ProfileName @@ -67,6 +70,7 @@ func FromDeviceModelToDTO(d models.Device) Device { dto.DBTimestamp = DBTimestamp(d.DBTimestamp) dto.Id = d.Id dto.Name = d.Name + dto.Parent = d.Parent dto.Description = d.Description dto.ServiceName = d.ServiceName dto.ProfileName = d.ProfileName @@ -88,6 +92,7 @@ func FromDeviceModelToUpdateDTO(d models.Device) UpdateDevice { dto := UpdateDevice{ Id: &d.Id, Name: &d.Name, + Parent: &d.Parent, Description: &d.Description, AdminState: &adminState, OperatingState: &operatingState, diff --git a/dtos/device_test.go b/dtos/device_test.go index 0ce76a51..2b62995c 100644 --- a/dtos/device_test.go +++ b/dtos/device_test.go @@ -13,17 +13,85 @@ import ( "github.com/stretchr/testify/assert" ) +var testId = "8b0ee7cb-7a94-4e21-bebf-55961071b060" +var testName = "DeviceName" +var testParent = "ParentName" +var testDescription = "Describe Me" +var testAdminState = "LOCKED" +var testOperatingState = "UP" +var testLocation = "Location" +var testServiceName = "ServiceName" +var testProfileName = "ProfileName" + +var testDeviceDto = Device{ + DBTimestamp: DBTimestamp{Created: 123, Modified: 456}, + Id: testId, + Name: testName, + Parent: testParent, + Description: testDescription, + AdminState: testAdminState, + OperatingState: testOperatingState, + Labels: []string{"label1", "label2"}, + Location: testLocation, + ServiceName: testServiceName, + ProfileName: testProfileName, + AutoEvents: []AutoEvent{{Interval: "5m", OnChange: false, SourceName: "sourceName"}}, + Protocols: map[string]ProtocolProperties{"protocol": {"key": "value"}}, + Tags: map[string]any{"tag": "value"}, + Properties: map[string]any{"property": "value"}, +} + +var testDeviceModel = models.Device{ + DBTimestamp: models.DBTimestamp{Created: 123, Modified: 456}, + Id: testId, + Name: testName, + Parent: testParent, + Description: testDescription, + AdminState: models.AdminState(testAdminState), + OperatingState: models.OperatingState(testOperatingState), + Labels: []string{"label1", "label2"}, + Location: testLocation, + ServiceName: testServiceName, + ProfileName: testProfileName, + AutoEvents: []models.AutoEvent{{Interval: "5m", OnChange: false, SourceName: "sourceName"}}, + Protocols: map[string]models.ProtocolProperties{"protocol": {"key": "value"}}, + Tags: map[string]any{"tag": "value"}, + Properties: map[string]any{"property": "value"}, +} + +var testUpdateDto = UpdateDevice{ + Id: &testId, + Name: &testName, + Parent: &testParent, + Description: &testDescription, + AdminState: &testAdminState, + OperatingState: &testOperatingState, + Labels: []string{"label1", "label2"}, + Location: testLocation, + ServiceName: &testServiceName, + ProfileName: &testProfileName, + AutoEvents: []AutoEvent{{Interval: "5m", OnChange: false, SourceName: "sourceName"}}, + Protocols: map[string]ProtocolProperties{"protocol": {"key": "value"}}, + Tags: map[string]any{"tag": "value"}, + Properties: map[string]any{"property": "value"}, +} + +func TestDeviceDTOtoModel(t *testing.T) { + dto := ToDeviceModel(testDeviceDto) + // All fields should propagate except DBTimestamp + testDeviceModelWithoutTime := testDeviceModel + testDeviceModelWithoutTime.DBTimestamp = models.DBTimestamp{} + assert.Equal(t, testDeviceModelWithoutTime, dto) +} + +func TestDeviceModeltoDTO(t *testing.T) { + model := testDeviceModel + dto := FromDeviceModelToDTO(model) + assert.Equal(t, testDeviceDto, dto) +} + func TestFromDeviceModelToUpdateDTO(t *testing.T) { - model := models.Device{} + model := testDeviceModel dto := FromDeviceModelToUpdateDTO(model) - assert.Equal(t, model.Id, *dto.Id) - assert.Equal(t, model.Name, *dto.Name) - assert.Equal(t, model.Description, *dto.Description) - assert.EqualValues(t, model.AdminState, *dto.AdminState) - assert.EqualValues(t, model.OperatingState, *dto.OperatingState) - assert.Equal(t, model.ServiceName, *dto.ServiceName) - assert.Equal(t, model.ProfileName, *dto.ProfileName) - assert.Equal(t, model.Location, dto.Location) - assert.Equal(t, model.Tags, dto.Tags) - assert.Equal(t, model.Properties, dto.Properties) + assert.Equal(t, testUpdateDto, dto) } diff --git a/dtos/requests/device.go b/dtos/requests/device.go index 4e739be8..9023e5fc 100644 --- a/dtos/requests/device.go +++ b/dtos/requests/device.go @@ -91,6 +91,9 @@ func ReplaceDeviceModelFieldsWithDTO(device *models.Device, patch dtos.UpdateDev if patch.Description != nil { device.Description = *patch.Description } + if patch.Parent != nil { + device.Parent = *patch.Parent + } if patch.AdminState != nil { device.AdminState = models.AdminState(*patch.AdminState) } diff --git a/dtos/requests/device_test.go b/dtos/requests/device_test.go index a78588e1..60851cd4 100644 --- a/dtos/requests/device_test.go +++ b/dtos/requests/device_test.go @@ -34,6 +34,7 @@ var testProtocols = map[string]dtos.ProtocolProperties{ "UnitID": "1", }, } +var testParent = "ParentDevice" var testAddDevice = AddDeviceRequest{ BaseRequest: dtoCommon.BaseRequest{ RequestId: ExampleUUID, @@ -49,6 +50,7 @@ var testAddDevice = AddDeviceRequest{ Location: testDeviceLocation, AutoEvents: testAutoEvents, Protocols: testProtocols, + Parent: testParent, }, } @@ -80,6 +82,7 @@ func mockUpdateDevice() dtos.UpdateDevice { d.Location = testDeviceLocation d.AutoEvents = testAutoEvents d.Protocols = testProtocols + d.Parent = &testParent return d } @@ -143,12 +146,15 @@ func TestAddDeviceRequest_Validate(t *testing.T) { profileNameWithUnreservedChars.Device.ProfileName = nameWithUnreservedChars serviceNameWithUnreservedChars := testAddDevice serviceNameWithUnreservedChars.Device.ServiceName = nameWithUnreservedChars + parentNameWithUnreservedChars := testAddDevice + parentNameWithUnreservedChars.Device.Parent = nameWithUnreservedChars // Following tests verify if name fields containing unreserved characters should pass edgex-dto-rfc3986-unreserved-chars check testsForNameFields := []testForNameField{ {"Valid AddDeviceRequest with device name containing unreserved chars", deviceNameWithUnreservedChars, false}, {"Valid AddDeviceRequest with profile name containing unreserved chars", profileNameWithUnreservedChars, false}, {"Valid AddDeviceRequest with service name containing unreserved chars", serviceNameWithUnreservedChars, false}, + {"Valid AddDeviceRequest with parent name containing unreserved chars", parentNameWithUnreservedChars, false}, } // Following tests verify if name fields containing reserved characters should not be detected with an error @@ -159,11 +165,14 @@ func TestAddDeviceRequest_Validate(t *testing.T) { profileNameWithReservedChar.Device.ProfileName = n serviceNameWithReservedChar := testAddDevice serviceNameWithReservedChar.Device.ServiceName = n + parentNameWithReservedChar := testAddDevice + parentNameWithReservedChar.Device.Parent = n testsForNameFields = append(testsForNameFields, testForNameField{"Valid AddDeviceRequest with device name containing reserved char", deviceNameWithReservedChar, false}, - testForNameField{"Valid AddDeviceRequest with device name containing reserved char", profileNameWithReservedChar, false}, - testForNameField{"Valid AddDeviceRequest with device name containing reserved char", serviceNameWithReservedChar, false}, + testForNameField{"Valid AddDeviceRequest with profile name containing reserved char", profileNameWithReservedChar, false}, + testForNameField{"Valid AddDeviceRequest with service name containing reserved char", serviceNameWithReservedChar, false}, + testForNameField{"Valid AddDeviceRequest with parent name containing reserved char", parentNameWithReservedChar, false}, ) } @@ -231,6 +240,7 @@ func Test_AddDeviceReqToDeviceModels(t *testing.T) { "UnitID": "1", }, }, + Parent: testParent, }, } resultModels := AddDeviceReqToDeviceModels(requests) diff --git a/models/device.go b/models/device.go index 13c7f89f..b825d2d5 100644 --- a/models/device.go +++ b/models/device.go @@ -9,6 +9,7 @@ type Device struct { DBTimestamp Id string Name string + Parent string Description string AdminState AdminState OperatingState OperatingState