diff --git a/Jenkinsfile b/Jenkinsfile index 77f5fc45..aaac6530 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -15,5 +15,6 @@ // edgeXBuildGoMod ( - project: 'go-mod-core-contracts' + project: 'go-mod-core-contracts', + goVersion: '1.23' ) \ No newline at end of file diff --git a/clients/http/deviceprofile.go b/clients/http/deviceprofile.go index 6fc9ae2d..ac46ac8a 100644 --- a/clients/http/deviceprofile.go +++ b/clients/http/deviceprofile.go @@ -48,6 +48,7 @@ func NewDeviceProfileClientWithUrlCallback(baseUrlFunc clients.ClientBaseUrlFunc return &DeviceProfileClient{ baseUrlFunc: baseUrlFunc, authInjector: authInjector, + resourcesCache: make(map[string]responses.DeviceResourceResponse), enableNameFieldEscape: enableNameFieldEscape, } } diff --git a/clients/http/deviceprofile_test.go b/clients/http/deviceprofile_test.go index f16ae7db..3a8489c5 100644 --- a/clients/http/deviceprofile_test.go +++ b/clients/http/deviceprofile_test.go @@ -24,12 +24,20 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/requests" "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/responses" edgexErrors "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestNewDeviceProfileClientWithUrlCallback(t *testing.T) { + baseUrlFunc := func() (string, error) { + return "", nil + } + client := NewDeviceProfileClientWithUrlCallback(baseUrlFunc, &emptyAuthenticationInjector{}, true) + require.NotNil(t, client) + require.NotNil(t, client.(*DeviceProfileClient).resourcesCache) +} + func TestAddDeviceProfiles(t *testing.T) { requestId := uuid.New().String() diff --git a/models/device.go b/models/device.go index 31942f46..de174dc4 100644 --- a/models/device.go +++ b/models/device.go @@ -1,10 +1,12 @@ // -// Copyright (C) 2020-2024 IOTech Ltd +// Copyright (C) 2020-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type Device struct { DBTimestamp Id string @@ -26,6 +28,12 @@ type Device struct { // ProtocolProperties contains the device connection information in key/value pair type ProtocolProperties map[string]any +func (p ProtocolProperties) Clone() ProtocolProperties { + cloned := make(map[string]any) + maps.Copy(cloned, p) + return cloned +} + // AdminState controls the range of values which constitute valid administrative states for a device type AdminState string @@ -39,3 +47,41 @@ func AssignAdminState(dtoAdminState string) AdminState { // OperatingState is an indication of the operations of the device. type OperatingState string + +func (device Device) Clone() Device { + cloned := Device{ + DBTimestamp: device.DBTimestamp, + Id: device.Id, + Name: device.Name, + Parent: device.Parent, + Description: device.Description, + AdminState: device.AdminState, + OperatingState: device.OperatingState, + Location: device.Location, + ServiceName: device.ServiceName, + ProfileName: device.ProfileName, + } + if len(device.Protocols) > 0 { + cloned.Protocols = make(map[string]ProtocolProperties) + for k, v := range device.Protocols { + cloned.Protocols[k] = v.Clone() + } + } + if len(device.Labels) > 0 { + cloned.Labels = make([]string, len(device.Labels)) + copy(cloned.Labels, device.Labels) + } + if len(device.AutoEvents) > 0 { + cloned.AutoEvents = make([]AutoEvent, len(device.AutoEvents)) + copy(cloned.AutoEvents, device.AutoEvents) + } + if len(device.Tags) > 0 { + cloned.Tags = make(map[string]any) + maps.Copy(cloned.Tags, device.Tags) + } + if len(device.Properties) > 0 { + cloned.Properties = make(map[string]any) + maps.Copy(cloned.Properties, device.Properties) + } + return cloned +} diff --git a/models/device_test.go b/models/device_test.go new file mode 100644 index 00000000..2c6c134b --- /dev/null +++ b/models/device_test.go @@ -0,0 +1,44 @@ +// Copyright (C) 2025 IOTech Ltd + +package models + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDevice_Clone(t *testing.T) { + testDevice := Device{ + DBTimestamp: DBTimestamp{}, + Id: "ca93c8fa-9919-4ec5-85d3-f81b2b6a7bc1", + Name: "testDevice", + Parent: "testParent", + Description: "testDescription", + AdminState: Locked, + OperatingState: Up, + Protocols: map[string]ProtocolProperties{"other": map[string]any{"Address": "127.0.0.1"}}, + Labels: []string{"label1", "label2"}, + Location: map[string]any{"loc": "x.y.z"}, + ServiceName: "testServiceName", + ProfileName: "testProfileName", + AutoEvents: []AutoEvent{ + { + Interval: "10s", + OnChange: false, + OnChangeThreshold: 0.5, + SourceName: "testSourceName", + Retention: Retention{ + MaxCap: 500, + MinCap: 100, + Duration: "1m", + }, + }, + }, + Tags: map[string]any{"tag1": "val1", "tag2": "val2"}, + Properties: map[string]any{ + "foo": "bar", + }, + } + clone := testDevice.Clone() + assert.Equal(t, testDevice, clone) +} diff --git a/models/devicecommand.go b/models/devicecommand.go index 590f2b77..fc0d6520 100644 --- a/models/devicecommand.go +++ b/models/devicecommand.go @@ -1,10 +1,12 @@ // -// Copyright (C) 2020-2023 IOTech Ltd +// Copyright (C) 2020-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type DeviceCommand struct { Name string IsHidden bool @@ -12,3 +14,22 @@ type DeviceCommand struct { ResourceOperations []ResourceOperation Tags map[string]any } + +func (dc DeviceCommand) Clone() DeviceCommand { + cloned := DeviceCommand{ + Name: dc.Name, + IsHidden: dc.IsHidden, + ReadWrite: dc.ReadWrite, + } + if len(dc.ResourceOperations) > 0 { + cloned.ResourceOperations = make([]ResourceOperation, len(dc.ResourceOperations)) + for i, op := range dc.ResourceOperations { + cloned.ResourceOperations[i] = op.Clone() + } + } + if len(dc.Tags) > 0 { + cloned.Tags = make(map[string]any) + maps.Copy(cloned.Tags, dc.Tags) + } + return cloned +} diff --git a/models/deviceprofile.go b/models/deviceprofile.go index 577fe32a..d828451a 100644 --- a/models/deviceprofile.go +++ b/models/deviceprofile.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2020-2024 IOTech Ltd +// Copyright (C) 2020-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -17,3 +17,32 @@ type DeviceProfile struct { DeviceResources []DeviceResource DeviceCommands []DeviceCommand } + +func (profile DeviceProfile) Clone() DeviceProfile { + cloned := DeviceProfile{ + DBTimestamp: profile.DBTimestamp, + ApiVersion: profile.ApiVersion, + Description: profile.Description, + Id: profile.Id, + Name: profile.Name, + Manufacturer: profile.Manufacturer, + Model: profile.Model, + } + if len(profile.Labels) > 0 { + cloned.Labels = make([]string, len(profile.Labels)) + copy(cloned.Labels, profile.Labels) + } + if len(profile.DeviceResources) > 0 { + cloned.DeviceResources = make([]DeviceResource, len(profile.DeviceResources)) + for i := range profile.DeviceResources { + cloned.DeviceResources[i] = profile.DeviceResources[i].Clone() + } + } + if len(profile.DeviceCommands) > 0 { + cloned.DeviceCommands = make([]DeviceCommand, len(profile.DeviceCommands)) + for i := range profile.DeviceCommands { + cloned.DeviceCommands[i] = profile.DeviceCommands[i].Clone() + } + } + return cloned +} diff --git a/models/deviceprofile_test.go b/models/deviceprofile_test.go new file mode 100644 index 00000000..acbc4544 --- /dev/null +++ b/models/deviceprofile_test.go @@ -0,0 +1,60 @@ +// Copyright (C) 2025 IOTech Ltd + +package models + +import ( + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDeviceProfile_Clone(t *testing.T) { + testMinimum := -1.123 + testMaximum := 1.123 + testDeviceProfile := DeviceProfile{ + DBTimestamp: DBTimestamp{}, + ApiVersion: common.ApiVersion, + Description: "test description", + Id: "ca93c8fa-9919-4ec5-85d3-f81b2b6a7bc1", + Name: "TestProfile", + Manufacturer: "testManufacturer", + Model: "testModel", + Labels: []string{"label1", "label2"}, + DeviceResources: []DeviceResource{{ + Description: "test description", + Name: "TestDeviceResource", + IsHidden: false, + Properties: ResourceProperties{ + ValueType: common.ValueTypeString, Minimum: &testMinimum, Maximum: &testMaximum}, + Attributes: map[string]any{ + "foo": "bar", + }, + Tags: map[string]any{ + "tag1": "val1", + }, + }}, + DeviceCommands: []DeviceCommand{{ + Name: "TestDeviceCommand", + IsHidden: false, + ReadWrite: "RW", + ResourceOperations: []ResourceOperation{{ + DeviceResource: "TestDeviceResource1", + DefaultValue: "", + Mappings: map[string]string{ + "on": "true", + }, + }, { + DeviceResource: "TestDeviceResource2", + DefaultValue: "", + Mappings: map[string]string{ + "off": "false", + }, + }}, + Tags: map[string]any{ + "tag3": "val3", + }, + }}, + } + clone := testDeviceProfile.Clone() + assert.Equal(t, testDeviceProfile, clone) +} diff --git a/models/deviceresource.go b/models/deviceresource.go index e4412a7c..903cb1e3 100644 --- a/models/deviceresource.go +++ b/models/deviceresource.go @@ -1,10 +1,12 @@ // -// Copyright (C) 2020-2023 IOTech Ltd +// Copyright (C) 2020-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type DeviceResource struct { Description string Name string @@ -13,3 +15,21 @@ type DeviceResource struct { Attributes map[string]interface{} Tags map[string]any } + +func (dr DeviceResource) Clone() DeviceResource { + cloned := DeviceResource{ + Description: dr.Description, + Name: dr.Name, + IsHidden: dr.IsHidden, + Properties: dr.Properties.Clone(), + } + if len(dr.Attributes) > 0 { + cloned.Attributes = make(map[string]any) + maps.Copy(cloned.Attributes, dr.Attributes) + } + if len(dr.Tags) > 0 { + cloned.Tags = make(map[string]any) + maps.Copy(cloned.Tags, dr.Tags) + } + return cloned +} diff --git a/models/discovereddevice.go b/models/discovereddevice.go index 8c612f1c..afd31846 100644 --- a/models/discovereddevice.go +++ b/models/discovereddevice.go @@ -1,13 +1,31 @@ // -// Copyright (C) 2023 IOTech Ltd +// Copyright (C) 2023-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type DiscoveredDevice struct { ProfileName string AdminState AdminState AutoEvents []AutoEvent Properties map[string]any } + +func (d DiscoveredDevice) Clone() DiscoveredDevice { + cloned := DiscoveredDevice{ + ProfileName: d.ProfileName, + AdminState: d.AdminState, + } + if len(d.AutoEvents) > 0 { + cloned.AutoEvents = make([]AutoEvent, len(d.AutoEvents)) + copy(cloned.AutoEvents, d.AutoEvents) + } + if len(d.Properties) > 0 { + cloned.Properties = make(map[string]any) + maps.Copy(cloned.Properties, d.Properties) + } + return cloned +} diff --git a/models/provisionwatcher.go b/models/provisionwatcher.go index 8346da6d..3244fc36 100644 --- a/models/provisionwatcher.go +++ b/models/provisionwatcher.go @@ -1,10 +1,12 @@ // -// Copyright (C) 2021-2023 IOTech Ltd +// Copyright (C) 2021-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type ProvisionWatcher struct { DBTimestamp Id string @@ -16,3 +18,30 @@ type ProvisionWatcher struct { AdminState AdminState DiscoveredDevice DiscoveredDevice } + +func (pw ProvisionWatcher) Clone() ProvisionWatcher { + cloned := ProvisionWatcher{ + DBTimestamp: pw.DBTimestamp, + Id: pw.Id, + Name: pw.Name, + ServiceName: pw.ServiceName, + AdminState: pw.AdminState, + DiscoveredDevice: pw.DiscoveredDevice.Clone(), + } + if len(pw.Labels) > 0 { + cloned.Labels = make([]string, len(pw.Labels)) + copy(cloned.Labels, pw.Labels) + } + if len(pw.Identifiers) > 0 { + cloned.Identifiers = make(map[string]string) + maps.Copy(cloned.Identifiers, pw.Identifiers) + } + if len(pw.BlockingIdentifiers) > 0 { + cloned.BlockingIdentifiers = make(map[string][]string) + for k, v := range pw.BlockingIdentifiers { + cloned.BlockingIdentifiers[k] = make([]string, len(v)) + copy(cloned.BlockingIdentifiers[k], v) + } + } + return cloned +} diff --git a/models/provisionwatcher_test.go b/models/provisionwatcher_test.go new file mode 100644 index 00000000..d517dc73 --- /dev/null +++ b/models/provisionwatcher_test.go @@ -0,0 +1,43 @@ +// Copyright (C) 2025 IOTech Ltd + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProvisionWatcher_Clone(t *testing.T) { + testProvisionWatcher := ProvisionWatcher{ + DBTimestamp: DBTimestamp{}, + Id: "ca93c8fa-9919-4ec5-85d3-f81b2b6a7bc1", + Name: "TestProvisionWatcher", + ServiceName: "TestServiceName", + Labels: []string{"label1", "label2"}, + Identifiers: map[string]string{"Address": "172.0.0.1", "Port": "8080"}, + BlockingIdentifiers: map[string][]string{"Address": {"127.0.0.1", "127.0.0.2"}, "Port": {"123", "456"}}, + AdminState: Unlocked, + DiscoveredDevice: DiscoveredDevice{ + ProfileName: "TestProfile", + AdminState: Locked, + AutoEvents: []AutoEvent{ + { + Interval: "10s", OnChange: false, OnChangeThreshold: 0.5, + SourceName: "TestDeviceResource", + Retention: Retention{MaxCap: 500, MinCap: 100, Duration: "1m"}, + }, + { + Interval: "15s", OnChange: true, OnChangeThreshold: 1.23, + SourceName: "TestDeviceResource2", + Retention: Retention{MaxCap: 1000, MinCap: 1, Duration: "1m"}, + }, + }, + Properties: map[string]any{ + "foo": "bar", + }, + }, + } + clone := testProvisionWatcher.Clone() + assert.Equal(t, testProvisionWatcher, clone) +} diff --git a/models/resourceoperation.go b/models/resourceoperation.go index a9a39b07..43388cd3 100644 --- a/models/resourceoperation.go +++ b/models/resourceoperation.go @@ -1,12 +1,26 @@ // -// Copyright (C) 2020 IOTech Ltd +// Copyright (C) 2020-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type ResourceOperation struct { DeviceResource string DefaultValue string Mappings map[string]string } + +func (r ResourceOperation) Clone() ResourceOperation { + cloned := ResourceOperation{ + DeviceResource: r.DeviceResource, + DefaultValue: r.DefaultValue, + } + if len(r.Mappings) > 0 { + cloned.Mappings = make(map[string]string) + maps.Copy(cloned.Mappings, r.Mappings) + } + return cloned +} diff --git a/models/resourceproperties.go b/models/resourceproperties.go index 98bc52cb..bba892d2 100644 --- a/models/resourceproperties.go +++ b/models/resourceproperties.go @@ -1,10 +1,12 @@ // -// Copyright (C) 2020-2023 IOTech Ltd +// Copyright (C) 2020-2025 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 package models +import "maps" + type ResourceProperties struct { ValueType string ReadWrite string @@ -21,3 +23,61 @@ type ResourceProperties struct { MediaType string Optional map[string]any } + +func (rp ResourceProperties) Clone() ResourceProperties { + var minimum *float64 + if rp.Minimum != nil { + val := *rp.Minimum + minimum = &val + } + var maximum *float64 + if rp.Maximum != nil { + val := *rp.Maximum + maximum = &val + } + var mask *uint64 + if rp.Mask != nil { + val := *rp.Mask + mask = &val + } + var shift *int64 + if rp.Shift != nil { + val := *rp.Shift + shift = &val + } + var scale *float64 + if rp.Scale != nil { + val := *rp.Scale + scale = &val + } + var offset *float64 + if rp.Offset != nil { + val := *rp.Offset + offset = &val + } + var base *float64 + if rp.Base != nil { + val := *rp.Base + base = &val + } + cloned := ResourceProperties{ + ValueType: rp.ValueType, + ReadWrite: rp.ReadWrite, + Units: rp.Units, + Minimum: minimum, + Maximum: maximum, + DefaultValue: rp.DefaultValue, + Mask: mask, + Shift: shift, + Scale: scale, + Offset: offset, + Base: base, + Assertion: rp.Assertion, + MediaType: rp.MediaType, + } + if len(rp.Optional) > 0 { + rp.Optional = make(map[string]any) + maps.Copy(rp.Optional, rp.Optional) + } + return cloned +}