Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/blang/semver/v4 v4.0.0
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.1.0-dev.45
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.21
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.22
github.com/edgexfoundry/go-mod-messaging/v4 v4.1.0-dev.18
github.com/edgexfoundry/go-mod-secrets/v4 v4.1.0-dev.7
github.com/fxamacker/cbor/v2 v2.9.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ github.com/edgexfoundry/go-mod-bootstrap/v4 v4.1.0-dev.45 h1:e8zhpWhjPfDypTmPxgM
github.com/edgexfoundry/go-mod-bootstrap/v4 v4.1.0-dev.45/go.mod h1:E9iUXkxMdTMXxyAN/MAr/srf8+ZbmtV+mVJvhW6a//k=
github.com/edgexfoundry/go-mod-configuration/v4 v4.1.0-dev.17 h1:TttwsEqLppEQQz4scPbEdzMtsHC8vj1djd2sgZLfny8=
github.com/edgexfoundry/go-mod-configuration/v4 v4.1.0-dev.17/go.mod h1:IlEPPn0CZX1mDBRX8E6nr7BM/MVxMZV5z9zSTo6fUgo=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.21 h1:gh+CoXbkXa2E3favumU513BYnB8U3ubW2zBUJpWNSwU=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.21/go.mod h1:jDm9E4z9svXErYAxr0oigmVV50wmIoHaveOlP7FBkHQ=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.22 h1:rV4aHYBoLvlyy9XHBDSC+cXbhiyRotF0RqZVI46Rd7I=
github.com/edgexfoundry/go-mod-core-contracts/v4 v4.1.0-dev.22/go.mod h1:jDm9E4z9svXErYAxr0oigmVV50wmIoHaveOlP7FBkHQ=
github.com/edgexfoundry/go-mod-messaging/v4 v4.1.0-dev.18 h1:iLlwJBZewKcoL+Ao4HQtcVrsfTtCEoGZRiNQjPGucPo=
github.com/edgexfoundry/go-mod-messaging/v4 v4.1.0-dev.18/go.mod h1:PcyJ06iPZfWH88+4Mmk8IJI2pDDclwdwI883wKoKocM=
github.com/edgexfoundry/go-mod-registry/v4 v4.1.0-dev.8 h1:swAEoWn8rr/NXcsBaCxoY+lWSiDUfZUmTyBsi9aM/7o=
Expand Down
27 changes: 27 additions & 0 deletions internal/core/metadata/application/deviceprofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,33 @@ func AllDeviceProfileBasicInfos(offset int, limit int, labels []string, dic *di.
return deviceProfileBasicInfos, totalCount, nil
}

func PatchDeviceProfileTags(profileName string, dto dtos.UpdateDeviceProfileTags, ctx context.Context, dic *di.Container) errors.EdgeX {
dbClient := container.DBClientFrom(dic.Get)
lc := bootstrapContainer.LoggingClientFrom(dic.Get)

deviceProfile, err := dbClient.DeviceProfileByName(profileName)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}

requests.ReplaceDeviceProfileModelTagsWithDTO(&deviceProfile, dto)

err = dbClient.UpdateDeviceProfile(deviceProfile)
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}

lc.Debugf(
"DeviceProfile device resources/commands tags patched on DB successfully. Correlation-ID: %s ",
correlation.FromContext(ctx),
)

profileDTO := dtos.FromDeviceProfileModelToDTO(deviceProfile)
go publishUpdateDeviceProfileSystemEvent(profileDTO, ctx, dic)

return nil
}

func deviceProfileByDTO(dbClient interfaces.DBClient, dto dtos.UpdateDeviceProfileBasicInfo) (deviceProfile models.DeviceProfile, err errors.EdgeX) {
// The ID or Name is required by DTO and the DTO also accepts empty string ID if the Name is provided
if dto.Id != nil && *dto.Id != "" {
Expand Down
31 changes: 31 additions & 0 deletions internal/core/metadata/controller/http/deviceprofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,34 @@ func (dc *DeviceProfileController) AllDeviceProfileBasicInfos(c echo.Context) er
utils.WriteHttpHeader(w, ctx, http.StatusOK)
return pkg.EncodeAndWriteResponse(response, w, lc)
}

func (dc *DeviceProfileController) PatchDeviceProfileTags(c echo.Context) error {
r := c.Request()
w := c.Response()
if r.Body != nil {
defer func() { _ = r.Body.Close() }()
}

lc := container.LoggingClientFrom(dc.dic.Get)

ctx := r.Context()

// URL parameters
name := c.Param(common.Name)

var reqDTO requestDTO.DeviceProfileTagsRequest
err := dc.jsonDtoReader.Read(r.Body, &reqDTO)
if err != nil {
return utils.WriteErrorResponse(w, ctx, lc, err, "")
}

reqId := reqDTO.RequestId
err = application.PatchDeviceProfileTags(name, reqDTO.UpdateDeviceProfileTags, ctx, dc.dic)
if err != nil {
return utils.WriteErrorResponse(w, ctx, lc, err, reqId)
}

response := commonDTO.NewBaseResponse(reqId, "", http.StatusOK)
utils.WriteHttpHeader(w, ctx, http.StatusOK)
return pkg.EncodeAndWriteResponse(response, w, lc)
}
105 changes: 105 additions & 0 deletions internal/core/metadata/controller/http/deviceprofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1605,3 +1605,108 @@ func TestAllDeviceProfileBasicInfos(t *testing.T) {
})
}
}

func TestPatchDeviceProfileTags(t *testing.T) {
deviceProfile := dtos.ToDeviceProfileModel(buildTestDeviceProfileRequest().Profile)
notFoundName := "notFoundName"
expectedRequestId := ExampleUUID
updateTags := map[string]any{"TestTagKey": "NewTestTagValue", "TestTagKey2": "TestTagValue2"}
testReq := requests.DeviceProfileTagsRequest{
BaseRequest: commonDTO.BaseRequest{
Versionable: commonDTO.NewVersionable(),
RequestId: ExampleUUID,
},
UpdateDeviceProfileTags: dtos.UpdateDeviceProfileTags{
DeviceResources: []dtos.UpdateTags{{Name: TestDeviceResourceName, Tags: updateTags}},
DeviceCommands: []dtos.UpdateTags{{Name: TestDeviceCommandName, Tags: updateTags}},
},
}

valid := testReq
noRequestId := valid
noRequestId.RequestId = ""

noDRName := testReq
noDRName.DeviceResources = []dtos.UpdateTags{{Tags: updateTags}}
emptyDRName := testReq
emptyDRName.DeviceResources = []dtos.UpdateTags{{Name: " ", Tags: updateTags}}
noDRTags := testReq
noDRTags.DeviceResources = []dtos.UpdateTags{{Name: TestDeviceResourceName}}
emptyDRTags := testReq
emptyDRTags.DeviceResources = []dtos.UpdateTags{{Name: TestDeviceCommandName, Tags: map[string]any{}}}

noDCName := testReq
noDCName.DeviceCommands = []dtos.UpdateTags{{Tags: updateTags}}
emptyDCName := testReq
emptyDCName.DeviceCommands = []dtos.UpdateTags{{Name: " ", Tags: updateTags}}
noDCTags := testReq
noDCTags.DeviceCommands = []dtos.UpdateTags{{Name: TestDeviceCommandName}}

emptyDCTags := testReq
emptyDCTags.DeviceCommands = []dtos.UpdateTags{{Name: TestDeviceCommandName, Tags: map[string]any{}}}

dic := mockDic()
dbClientMock := &mocks.DBClient{}
dbClientMock.On("DeviceProfileByName", deviceProfile.Name).Return(deviceProfile, nil)
dbClientMock.On("DeviceProfileByName", notFoundName).Return(deviceProfile, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, "not found", nil))
dbClientMock.On("DevicesByProfileName", 0, -1, deviceProfile.Name).Return([]models.Device{{ServiceName: testDeviceServiceName}}, nil)
dbClientMock.On("DeviceCountByProfileName", deviceProfile.Name).Return(int64(1), nil)
dbClientMock.On("UpdateDeviceProfile", mock.Anything).Return(nil)
dic.Update(di.ServiceConstructorMap{
container.DBClientInterfaceName: func(get di.Get) interface{} {
return dbClientMock
},
})

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

tests := []struct {
name string
deviceProfileName string
request requests.DeviceProfileTagsRequest
expectedStatusCode int
}{
{"valid", deviceProfile.Name, valid, http.StatusOK},
{"valid - no request id", deviceProfile.Name, noRequestId, http.StatusOK},
{"invalid - device profile not found", notFoundName, valid, http.StatusNotFound},
{"invalid - no device resource name", deviceProfile.Name, noDRName, http.StatusBadRequest},
{"invalid - empty device resource name", deviceProfile.Name, emptyDRName, http.StatusBadRequest},
{"invalid - no device resource tags", deviceProfile.Name, noDRTags, http.StatusBadRequest},
{"invalid - empty device resource tags", deviceProfile.Name, emptyDRTags, http.StatusBadRequest},
{"invalid - no device command name", deviceProfile.Name, noDCName, http.StatusBadRequest},
{"invalid - empty device command name", deviceProfile.Name, emptyDCName, http.StatusBadRequest},
{"invalid - no device command tags", deviceProfile.Name, noDRTags, http.StatusBadRequest},
{"invalid - empty device command tags", deviceProfile.Name, emptyDCTags, http.StatusBadRequest},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
e := echo.New()
jsonData, err := json.Marshal(testCase.request)
require.NoError(t, err)

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

// Act
recorder := httptest.NewRecorder()
c := e.NewContext(req, recorder)
c.SetParamNames(common.Name)
c.SetParamValues(testCase.deviceProfileName)
err = controller.PatchDeviceProfileTags(c)
require.NoError(t, err)

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

assert.NotEmpty(t, recorder.Body.String(), "Message is empty")
assert.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected")
assert.Equal(t, testCase.expectedStatusCode, recorder.Result().StatusCode, "HTTP status code not as expected")
if res.RequestId != "" {
assert.Equal(t, expectedRequestId, res.RequestId, "RequestID not as expected")
}
})
}
}
1 change: 1 addition & 0 deletions internal/core/metadata/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func LoadRestRoutes(r *echo.Echo, dic *di.Container, serviceName string) {
r.GET(common.ApiDeviceProfileByManufacturerAndModelRoute, dc.DeviceProfilesByManufacturerAndModel, authenticationHook)
r.PATCH(common.ApiDeviceProfileBasicInfoRoute, dc.PatchDeviceProfileBasicInfo, authenticationHook)
r.GET(common.ApiAllDeviceProfileBasicInfoRoute, dc.AllDeviceProfileBasicInfos, authenticationHook)
r.PATCH(common.ApiDeviceProfileTagsByNameRoute, dc.PatchDeviceProfileTags, authenticationHook)

// Device Resource
dr := metadataController.NewDeviceResourceController(dic)
Expand Down
98 changes: 95 additions & 3 deletions openapi/core-metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ components:
properties:
type: object
description: A map of properties required to address the given device

DeviceProfileBasicInfo:
description: "A profile basic information"
type: object
Expand Down Expand Up @@ -336,6 +335,32 @@ components:
$ref: '#/components/schemas/DeviceProfileBasicInfo'
required:
- profileName
UpdateTags:
type: object
properties:
name:
type: string
description: device resource or device command name
tags:
type: object
description: A map of tags to add or update
required:
- name
- tags
DeviceProfileTagsRequest:
description: "Add/Update tags of device resources/device commands in an existing profile"
type: object
allOf:
- $ref: '#/components/schemas/BaseRequest'
properties:
deviceResources:
type: array
items:
$ref: '#/components/schemas/UpdateTags'
deviceCommands:
type: array
items:
$ref: '#/components/schemas/UpdateTags'
MultiDeviceProfileBasicInfosResponse:
allOf:
- $ref: '#/components/schemas/BaseWithTotalCountResponse'
Expand Down Expand Up @@ -446,7 +471,7 @@ components:
type: boolean
description: "Indicate the visibility of the DeviceResource via a CoreCommand."
tags:
type: string
type: object
description: "Tags for adding additional information on reading level"
properties:
$ref: '#/components/schemas/ResourceProperties'
Expand Down Expand Up @@ -853,7 +878,7 @@ components:
name:
type: string
tags:
type: string
type: object
description: "Tags for adding additional information on event level"
isHidden:
type: boolean
Expand Down Expand Up @@ -2671,6 +2696,73 @@ paths:
examples:
500Example:
$ref: '#/components/examples/500Example'
'/deviceprofile/name/{name}/tags':
parameters:
- $ref: '#/components/parameters/correlatedRequestHeader'
- name: name
in: path
required: true
schema:
type: string
description: "The unique name of a device profile"
patch:
summary: "Allows adding or updating the tags field for device resources or device commands within an existing device profile. Removing existing tags is not supported."
requestBody:
required: true
content:
application/json:
schema:
type: object
$ref: '#/components/schemas/DeviceProfileTagsRequest'
example:
apiVersion: "v3"
requestId: "2463bff9-aa53-4bc4-bebf-42fe81146ea8"
deviceResources:
- name: "Float32"
tags:
tag1: "field1Value"
deviceCommands:
- name: "Float32"
tags:
tag2:
field3: "field3Value"
responses:
'200':
description: "Update successful"
headers:
X-Correlation-ID:
$ref: '#/components/headers/correlatedResponseHeader'
content:
application/json:
schema:
$ref: '#/components/schemas/BaseResponse'
examples:
200Example:
$ref: '#/components/examples/200Example'
'400':
description: "Request is in an invalid state"
headers:
X-Correlation-ID:
$ref: '#/components/headers/correlatedResponseHeader'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
400Example:
$ref: '#/components/examples/400Example'
'500':
description: An unexpected error occurred on the server
headers:
X-Correlation-ID:
$ref: '#/components/headers/correlatedResponseHeader'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
500Example:
$ref: '#/components/examples/500Example'
'/deviceprofile/basicinfo':
parameters:
- $ref: '#/components/parameters/correlatedRequestHeader'
Expand Down
Loading