Skip to content

Commit

Permalink
feat(metadata): implement Unit of Measure (UoM) ADR (#4119)
Browse files Browse the repository at this point in the history
* feat(metadata): implement Unit of Measure (UoM) ADR

Signed-off-by: Chris Hung <chris@iotechsys.com>

* fix(snap): fix PR snap test error

Signed-off-by: Chris Hung <chris@iotechsys.com>

Signed-off-by: Chris Hung <chris@iotechsys.com>
  • Loading branch information
Chris Hung authored Aug 12, 2022
1 parent e3ca38a commit 03487ec
Show file tree
Hide file tree
Showing 15 changed files with 314 additions and 15 deletions.
1 change: 1 addition & 0 deletions cmd/core-metadata/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ WORKDIR /
COPY --from=builder /edgex-go/Attribution.txt /
COPY --from=builder /edgex-go/cmd/core-metadata/core-metadata /
COPY --from=builder /edgex-go/cmd/core-metadata/res/configuration.toml /res/configuration.toml
COPY --from=builder /edgex-go/cmd/core-metadata/res/uom.toml /res/uom.toml

ENTRYPOINT ["/core-metadata"]
CMD ["-cp=consul.http://edgex-core-consul:8500", "--registry", "--confdir=/res"]
5 changes: 5 additions & 0 deletions cmd/core-metadata/res/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ LogLevel = "INFO"
[Writable.ProfileChange]
StrictDeviceProfileChanges = false
StrictDeviceProfileDeletes = false
[Writable.UoM]
Validation = false
[Writable.InsecureSecrets]
[Writable.InsecureSecrets.DB]
path = "redisdb"
Expand All @@ -33,6 +35,9 @@ RequestTimeout = "5s"
CORSExposeHeaders = "Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma, X-Correlation-ID"
CORSMaxAge = 3600

[UoM]
UoMFile = "./res/uom.toml"

[Registry]
Host = "localhost"
Port = 8500
Expand Down
9 changes: 9 additions & 0 deletions cmd/core-metadata/res/uom.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Uom]
Source="reference to source for all UoM if not specified below"
[Uom.Units]
[Uom.Units.temperature]
Source="www.weather.com"
Values=["C","F","K"]
[Uom.Units.weights]
Source="www.usa.gov/federal-agencies/weights-and-measures-division"
Values=["lbs","ounces","kilos","grams"]
23 changes: 23 additions & 0 deletions internal/core/metadata/application/deviceprofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func AddDeviceProfile(d models.DeviceProfile, ctx context.Context, dic *di.Conta
dbClient := container.DBClientFrom(dic.Get)
lc := bootstrapContainer.LoggingClientFrom(dic.Get)

err = deviceProfileUoMValidation(d, dic)
if err != nil {
return "", errors.NewCommonEdgeXWrapper(err)
}

correlationId := correlation.FromContext(ctx)
addedDeviceProfile, err := dbClient.AddDeviceProfile(d)
if err != nil {
Expand All @@ -50,6 +55,11 @@ func UpdateDeviceProfile(d models.DeviceProfile, ctx context.Context, dic *di.Co
dbClient := container.DBClientFrom(dic.Get)
lc := bootstrapContainer.LoggingClientFrom(dic.Get)

err = deviceProfileUoMValidation(d, dic)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}

err = dbClient.UpdateDeviceProfile(d)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
Expand Down Expand Up @@ -229,3 +239,16 @@ func deviceProfileByDTO(dbClient interfaces.DBClient, dto dtos.UpdateDeviceProfi
}
return deviceProfile, nil
}

func deviceProfileUoMValidation(p models.DeviceProfile, dic *di.Container) errors.EdgeX {
if container.ConfigurationFrom(dic.Get).Writable.UoM.Validation {
uom := container.UnitsOfMeasureFrom(dic.Get)
for _, dr := range p.DeviceResources {
if ok := uom.Validate(dr.Properties.Units); !ok {
return errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("DeviceResource %s units %s is invalid", dr.Name, dr.Properties.Units), nil)
}
}
}

return nil
}
10 changes: 10 additions & 0 deletions internal/core/metadata/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ type ConfigurationStruct struct {
Service bootstrapConfig.ServiceInfo
MessageQueue bootstrapConfig.MessageBusInfo
SecretStore bootstrapConfig.SecretStoreInfo
UoM UoM
}

type WritableInfo struct {
LogLevel string
ProfileChange ProfileChange
UoM WritableUoM
InsecureSecrets bootstrapConfig.InsecureSecrets
}

Expand All @@ -43,6 +45,14 @@ type ProfileChange struct {
StrictDeviceProfileDeletes bool
}

type WritableUoM struct {
Validation bool
}

type UoM struct {
UoMFile string
}

// NotificationInfo provides properties related to the assembly of notification content
type NotificationInfo struct {
Content string
Expand Down
20 changes: 20 additions & 0 deletions internal/core/metadata/container/uom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Copyright (C) 2022 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package container

import (
"github.com/edgexfoundry/go-mod-bootstrap/v2/di"

"github.com/edgexfoundry/edgex-go/internal/core/metadata/infrastructure/interfaces"
)

// UnitsOfMeasureInterfaceName contains the name of the interfaces.UnitsOfMeasure implementation in the DIC.
var UnitsOfMeasureInterfaceName = di.TypeInstanceToName((*interfaces.UnitsOfMeasure)(nil))

// UnitsOfMeasureFrom helper function queries the DIC and returns the interfaces.UnitsOfMeasure implementation.
func UnitsOfMeasureFrom(get di.Get) interfaces.UnitsOfMeasure {
return get(UnitsOfMeasureInterfaceName).(interfaces.UnitsOfMeasure)
}
3 changes: 2 additions & 1 deletion internal/core/metadata/controller/http/const_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2020 IOTech Ltd
// Copyright (C) 2020-2022 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -12,6 +12,7 @@ const (
TestDescription = "TestDescription"
TestModel = "TestModel"
TestDeviceResourceName = "TestDeviceResourceName"
TestUnits = "TestUnits"
TestTag = "TestTag"
TestDeviceCommandName = "TestDeviceCommand"
TestDeviceName = "TestDevice"
Expand Down
108 changes: 94 additions & 14 deletions internal/core/metadata/controller/http/deviceprofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (

"github.com/edgexfoundry/edgex-go/internal/core/metadata/config"
"github.com/edgexfoundry/edgex-go/internal/core/metadata/container"
dbMock "github.com/edgexfoundry/edgex-go/internal/core/metadata/infrastructure/interfaces/mocks"
"github.com/edgexfoundry/edgex-go/internal/core/metadata/infrastructure/interfaces/mocks"

"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
Expand All @@ -55,6 +55,7 @@ func buildTestDeviceProfileRequest() requests.DeviceProfileRequest {
Properties: dtos.ResourceProperties{
ValueType: common.ValueTypeInt16,
ReadWrite: common.ReadWrite_RW,
Units: TestUnits,
},
}, {
Name: TestDeviceResourceName + "-dup",
Expand All @@ -64,6 +65,7 @@ func buildTestDeviceProfileRequest() requests.DeviceProfileRequest {
Properties: dtos.ResourceProperties{
ValueType: common.ValueTypeInt16,
ReadWrite: common.ReadWrite_RW,
Units: TestUnits,
},
}}
var testDeviceCommands = []dtos.DeviceCommand{{
Expand Down Expand Up @@ -169,7 +171,7 @@ func TestAddDeviceProfile_Created(t *testing.T) {
expectedRequestId := ExampleUUID

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("AddDeviceProfile", deviceProfileModel).Return(deviceProfileModel, nil)
dic.Update(di.ServiceConstructorMap{
container.DBClientInterfaceName: func(get di.Get) interface{} {
Expand Down Expand Up @@ -308,7 +310,7 @@ func TestAddDeviceProfile_Duplicated(t *testing.T) {
duplicateNameDBError := errors.NewCommonEdgeX(errors.KindDuplicateName, fmt.Sprintf("device profile name %s exists", duplicateNameModel.Name), nil)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("AddDeviceProfile", duplicateNameModel).Return(duplicateNameModel, duplicateNameDBError)
dbClientMock.On("AddDeviceProfile", duplicateIdModel).Return(duplicateIdModel, duplicateIdDBError)
dic.Update(di.ServiceConstructorMap{
Expand Down Expand Up @@ -356,6 +358,84 @@ func TestAddDeviceProfile_Duplicated(t *testing.T) {
}
}

func TestAddDeviceProfile_UnitsOfMeasure_Validation(t *testing.T) {
deviceProfileRequest := buildTestDeviceProfileRequest()
deviceProfileModel := requests.DeviceProfileReqToDeviceProfileModel(deviceProfileRequest)
expectedRequestId := ExampleUUID

emptyUnits := ""
invalidUnits := "invalid"
validReq := deviceProfileRequest
emptyUnitsReq := buildTestDeviceProfileRequest()
for i := range emptyUnitsReq.Profile.DeviceResources {
emptyUnitsReq.Profile.DeviceResources[i].Properties.Units = emptyUnits
}
noUnitsModel := requests.DeviceProfileReqToDeviceProfileModel(emptyUnitsReq)
invalidUnitsReq := buildTestDeviceProfileRequest()
for i := range invalidUnitsReq.Profile.DeviceResources {
invalidUnitsReq.Profile.DeviceResources[i].Properties.Units = invalidUnits
}

dic := mockDic()
container.ConfigurationFrom(dic.Get).Writable.UoM.Validation = true
dbClientMock := &mocks.DBClient{}
dbClientMock.On("AddDeviceProfile", deviceProfileModel).Return(deviceProfileModel, nil)
dbClientMock.On("AddDeviceProfile", noUnitsModel).Return(noUnitsModel, nil)
uomMock := &mocks.UnitsOfMeasure{}
uomMock.On("Validate", TestUnits).Return(true)
uomMock.On("Validate", "").Return(true)
uomMock.On("Validate", "invalid").Return(false)
dic.Update(di.ServiceConstructorMap{
container.DBClientInterfaceName: func(get di.Get) interface{} {
return dbClientMock
},
container.UnitsOfMeasureInterfaceName: func(get di.Get) interface{} {
return uomMock
},
})

controller := NewDeviceProfileController(dic)
assert.NotNil(t, controller)

tests := []struct {
name string
Request []requests.DeviceProfileRequest
expectedStatusCode int
}{
{"valid - expected units", []requests.DeviceProfileRequest{validReq}, http.StatusCreated},
{"valid - units not provided", []requests.DeviceProfileRequest{emptyUnitsReq}, http.StatusCreated},
{"invalid - unexpected units", []requests.DeviceProfileRequest{invalidUnitsReq}, http.StatusInternalServerError},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
jsonData, err := json.Marshal(testCase.Request)
require.NoError(t, err)

reader := strings.NewReader(string(jsonData))
req, err := http.NewRequest(http.MethodPost, common.ApiDeviceProfileRoute, reader)
require.NoError(t, err)

// Act
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(controller.AddDeviceProfile)
handler.ServeHTTP(recorder, req)

var res []commonDTO.BaseResponse
err = json.Unmarshal(recorder.Body.Bytes(), &res)
require.NoError(t, err)

// Assert
assert.Equal(t, http.StatusMultiStatus, recorder.Result().StatusCode, "HTTP status code not as expected")
assert.Equal(t, common.ApiVersion, res[0].ApiVersion, "API Version not as expected")
if res[0].RequestId != "" {
assert.Equal(t, expectedRequestId, res[0].RequestId, "RequestID not as expected")
}
assert.Equal(t, testCase.expectedStatusCode, res[0].StatusCode, "BaseResponse status code not as expected")
assert.NotEmpty(t, recorder.Body.String(), "Message is empty")
})
}
}

func TestUpdateDeviceProfile(t *testing.T) {
deviceProfileRequest := buildTestDeviceProfileRequest()
deviceProfileModel := requests.DeviceProfileReqToDeviceProfileModel(deviceProfileRequest)
Expand Down Expand Up @@ -402,7 +482,7 @@ func TestUpdateDeviceProfile(t *testing.T) {
notFoundDBError := errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, fmt.Sprintf("device profile %s does not exists", notFound.Profile.Name), nil)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("UpdateDeviceProfile", deviceProfileModel).Return(nil)
dbClientMock.On("UpdateDeviceProfile", notFoundDeviceProfileModel).Return(notFoundDBError)
dbClientMock.On("DeviceCountByProfileName", deviceProfileModel.Name).Return(uint32(1), nil)
Expand Down Expand Up @@ -537,7 +617,7 @@ func TestPatchDeviceProfileBasicInfo(t *testing.T) {
notFound.BasicInfo.Name = &notFoundName

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfileById", *valid.BasicInfo.Id).Return(dpModel, nil)
dbClientMock.On("DeviceProfileByName", *valid.BasicInfo.Name).Return(dpModel, nil)
dbClientMock.On("DeviceProfileByName", notFoundName).Return(dpModel, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, "not found", nil))
Expand Down Expand Up @@ -610,7 +690,7 @@ func TestAddDeviceProfileByYaml_Created(t *testing.T) {
deviceProfileModel := dtos.ToDeviceProfileModel(deviceProfileDTO)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("AddDeviceProfile", deviceProfileModel).Return(deviceProfileModel, nil)
dic.Update(di.ServiceConstructorMap{
container.DBClientInterfaceName: func(get di.Get) interface{} {
Expand Down Expand Up @@ -721,7 +801,7 @@ func TestAddDeviceProfileByYaml_Duplicated(t *testing.T) {
dbError := errors.NewCommonEdgeX(errors.KindDuplicateName, fmt.Sprintf("device profile %s already exists", TestDeviceProfileName), nil)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("AddDeviceProfile", deviceProfileModel).Return(deviceProfileModel, dbError)
dic.Update(di.ServiceConstructorMap{
container.DBClientInterfaceName: func(get di.Get) interface{} {
Expand Down Expand Up @@ -825,7 +905,7 @@ func TestUpdateDeviceProfileByYaml(t *testing.T) {
notFoundDBError := errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, fmt.Sprintf("device profile %s does not exists", notFoundDeviceProfileModel.Name), nil)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("UpdateDeviceProfile", validDeviceProfileModel).Return(nil)
dbClientMock.On("UpdateDeviceProfile", notFoundDeviceProfileModel).Return(notFoundDBError)
dbClientMock.On("DeviceCountByProfileName", validDeviceProfileModel.Name).Return(uint32(1), nil)
Expand Down Expand Up @@ -920,7 +1000,7 @@ func TestDeviceProfileByName(t *testing.T) {
notFoundName := "notFoundName"

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfileByName", deviceProfile.Name).Return(deviceProfile, nil)
dbClientMock.On("DeviceProfileByName", notFoundName).Return(models.DeviceProfile{}, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, "device profile doesn't exist in the database", nil))
dic.Update(di.ServiceConstructorMap{
Expand Down Expand Up @@ -985,7 +1065,7 @@ func TestDeleteDeviceProfileByName(t *testing.T) {
provisionWatcherExists := "provisionWatcherExists"

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DevicesByProfileName", 0, 1, deviceProfile.Name).Return([]models.Device{}, nil)
dbClientMock.On("ProvisionWatchersByProfileName", 0, 1, deviceProfile.Name).Return([]models.ProvisionWatcher{}, nil)
dbClientMock.On("DeleteDeviceProfileByName", deviceProfile.Name).Return(nil)
Expand Down Expand Up @@ -1083,7 +1163,7 @@ func TestAllDeviceProfiles(t *testing.T) {
expectedTotalProfileCount := uint32(3)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfileCountByLabels", []string(nil)).Return(expectedTotalProfileCount, nil)
dbClientMock.On("DeviceProfileCountByLabels", testDeviceProfileLabels).Return(expectedTotalProfileCount, nil)
dbClientMock.On("AllDeviceProfiles", 0, 10, []string(nil)).Return(deviceProfiles, nil)
Expand Down Expand Up @@ -1160,7 +1240,7 @@ func TestDeviceProfilesByModel(t *testing.T) {
expectedTotalProfileCount := uint32(3)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfileCountByModel", TestModel).Return(expectedTotalProfileCount, nil)
dbClientMock.On("DeviceProfilesByModel", 0, 10, TestModel).Return(deviceProfiles, nil)
dbClientMock.On("DeviceProfilesByModel", 1, 2, TestModel).Return([]models.DeviceProfile{deviceProfiles[1], deviceProfiles[2]}, nil)
Expand Down Expand Up @@ -1233,7 +1313,7 @@ func TestDeviceProfilesByManufacturer(t *testing.T) {
expectedTotalProfileCount := uint32(3)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfileCountByManufacturer", TestManufacturer).Return(expectedTotalProfileCount, nil)
dbClientMock.On("DeviceProfilesByManufacturer", 0, 10, TestManufacturer).Return(deviceProfiles, nil)
dbClientMock.On("DeviceProfilesByManufacturer", 1, 2, TestManufacturer).Return([]models.DeviceProfile{deviceProfiles[1], deviceProfiles[2]}, nil)
Expand Down Expand Up @@ -1306,7 +1386,7 @@ func TestDeviceProfilesByManufacturerAndModel(t *testing.T) {
expectedTotalProfileCount := uint32(3)

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfilesByManufacturerAndModel", 0, 10, TestManufacturer, TestModel).Return(deviceProfiles, expectedTotalProfileCount, nil)
dbClientMock.On("DeviceProfilesByManufacturerAndModel", 1, 2, TestManufacturer, TestModel).Return([]models.DeviceProfile{deviceProfiles[1], deviceProfiles[2]}, expectedTotalProfileCount, nil)
dbClientMock.On("DeviceProfilesByManufacturerAndModel", 4, 1, TestManufacturer, TestModel).Return([]models.DeviceProfile{}, expectedTotalProfileCount, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, "query objects bounds out of range.", nil))
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 03487ec

Please sign in to comment.