diff --git a/go.mod b/go.mod index 45a5d96103..4b04113ace 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/edgexfoundry/go-mod-bootstrap v0.0.60 github.com/edgexfoundry/go-mod-configuration v0.0.8 - github.com/edgexfoundry/go-mod-core-contracts v0.1.118 + github.com/edgexfoundry/go-mod-core-contracts v0.1.119 github.com/edgexfoundry/go-mod-messaging v0.1.28 github.com/edgexfoundry/go-mod-registry v0.1.26 github.com/edgexfoundry/go-mod-secrets v0.0.26 diff --git a/internal/core/data/v2/controller/http/const_test.go b/internal/core/data/v2/controller/http/const_test.go index 81a6962370..fbe6a38a89 100644 --- a/internal/core/data/v2/controller/http/const_test.go +++ b/internal/core/data/v2/controller/http/const_test.go @@ -8,6 +8,7 @@ package http const ( ExampleUUID = "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" TestDeviceName = "TestDevice" + TestProfileName = "TestProfileName" TestPushedTime = 1600666231295 TestCreatedTime = 1600666214495 TestOriginTime = 1600666185705354000 diff --git a/internal/core/data/v2/controller/http/event_test.go b/internal/core/data/v2/controller/http/event_test.go index cee31d478c..f8f66a448c 100644 --- a/internal/core/data/v2/controller/http/event_test.go +++ b/internal/core/data/v2/controller/http/event_test.go @@ -145,10 +145,6 @@ func TestAddEvent(t *testing.T) { noSimpleValue := validRequest noSimpleValue.Event.Readings = []dtos.BaseReading{testReading} noSimpleValue.Event.Readings[0].Value = "" - noSimpleFloatEnconding := validRequest - noSimpleFloatEnconding.Event.Readings = []dtos.BaseReading{testReading} - noSimpleFloatEnconding.Event.Readings[0].ValueType = dtos.ValueTypeFloat32 - noSimpleFloatEnconding.Event.Readings[0].FloatEncoding = "" noBinaryValue := validRequest noBinaryValue.Event.Readings = []dtos.BaseReading{{ DeviceName: TestDeviceName, @@ -196,7 +192,6 @@ func TestAddEvent(t *testing.T) { {"Invalid - No Reading ValueType", []requests.AddEventRequest{noReadingValueType}, true, http.StatusBadRequest}, {"Invalid - Invalid Reading ValueType", []requests.AddEventRequest{invalidReadingInvalidValueType}, true, http.StatusBadRequest}, {"Invalid - No SimpleReading Value", []requests.AddEventRequest{noSimpleValue}, true, http.StatusBadRequest}, - {"Invalid - No SimpleReading FloatEncoding", []requests.AddEventRequest{noSimpleFloatEnconding}, true, http.StatusBadRequest}, {"Invalid - No BinaryReading BinaryValue", []requests.AddEventRequest{noBinaryValue}, true, http.StatusBadRequest}, {"Invalid - No BinaryReading MediaType", []requests.AddEventRequest{noBinaryMediaType}, true, http.StatusBadRequest}, } diff --git a/internal/core/metadata/v2/application/deviceprofile.go b/internal/core/metadata/v2/application/deviceprofile.go index 7f7309e37f..f70296ab8d 100644 --- a/internal/core/metadata/v2/application/deviceprofile.go +++ b/internal/core/metadata/v2/application/deviceprofile.go @@ -118,3 +118,20 @@ func AllDeviceProfiles(offset int, limit int, labels []string, dic *di.Container } return deviceProfiles, nil } + +// DeviceProfilesByModel query the device profiles with offset, limit and model +func DeviceProfilesByModel(offset int, limit int, model string, dic *di.Container) (deviceProfiles []dtos.DeviceProfile, err errors.EdgeX) { + if model == "" { + return deviceProfiles, errors.NewCommonEdgeX(errors.KindContractInvalid, "model is empty", nil) + } + dbClient := v2MetadataContainer.DBClientFrom(dic.Get) + dps, err := dbClient.DeviceProfilesByModel(offset, limit, model) + if err != nil { + return deviceProfiles, errors.NewCommonEdgeXWrapper(err) + } + deviceProfiles = make([]dtos.DeviceProfile, len(dps)) + for i, dp := range dps { + deviceProfiles[i] = dtos.FromDeviceProfileModelToDTO(dp) + } + return deviceProfiles, nil +} diff --git a/internal/core/metadata/v2/controller/http/deviceprofile.go b/internal/core/metadata/v2/controller/http/deviceprofile.go index f86b48ecae..cccaf90afc 100644 --- a/internal/core/metadata/v2/controller/http/deviceprofile.go +++ b/internal/core/metadata/v2/controller/http/deviceprofile.go @@ -15,7 +15,7 @@ import ( "github.com/edgexfoundry/go-mod-bootstrap/di" "github.com/edgexfoundry/go-mod-core-contracts/clients" "github.com/edgexfoundry/go-mod-core-contracts/errors" - contractsV2 "github.com/edgexfoundry/go-mod-core-contracts/v2" + "github.com/edgexfoundry/go-mod-core-contracts/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" requestDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/requests" @@ -238,7 +238,7 @@ func (dc *DeviceProfileController) DeviceProfileByName(w http.ResponseWriter, r // URL parameters vars := mux.Vars(r) - name := vars[contractsV2.Name] + name := vars[v2.Name] var response interface{} var statusCode int @@ -267,7 +267,7 @@ func (dc *DeviceProfileController) DeleteDeviceProfileById(w http.ResponseWriter // URL parameters vars := mux.Vars(r) - id := vars[contractsV2.Id] + id := vars[v2.Id] var response interface{} var statusCode int @@ -297,7 +297,7 @@ func (dc *DeviceProfileController) DeleteDeviceProfileByName(w http.ResponseWrit // URL parameters vars := mux.Vars(r) - name := vars[contractsV2.Name] + name := vars[v2.Name] var response interface{} var statusCode int @@ -354,3 +354,41 @@ func (dc *DeviceProfileController) AllDeviceProfiles(w http.ResponseWriter, r *h utils.WriteHttpHeader(w, ctx, statusCode) pkg.Encode(response, w, lc) } + +func (dc *DeviceProfileController) DeviceProfilesByModel(w http.ResponseWriter, r *http.Request) { + lc := container.LoggingClientFrom(dc.dic.Get) + ctx := r.Context() + correlationId := correlation.FromContext(ctx) + config := metadataContainer.ConfigurationFrom(dc.dic.Get) + + vars := mux.Vars(r) + model := vars[v2.Model] + + var response interface{} + var statusCode int + + // parse URL query string for offset, limit + offset, limit, _, err := utils.ParseGetAllObjectsRequestQueryString(r, 0, math.MaxInt32, -1, config.Service.MaxResultCount) + if err != nil { + lc.Error(err.Error(), clients.CorrelationHeader, correlationId) + lc.Debug(err.DebugMessages(), clients.CorrelationHeader, correlationId) + response = commonDTO.NewBaseResponse("", err.Message(), err.Code()) + statusCode = err.Code() + } else { + deviceProfiles, err := application.DeviceProfilesByModel(offset, limit, model, dc.dic) + if err != nil { + if errors.Kind(err) != errors.KindEntityDoesNotExist { + lc.Error(err.Error(), clients.CorrelationHeader, correlationId) + } + lc.Debug(err.DebugMessages(), clients.CorrelationHeader, correlationId) + response = commonDTO.NewBaseResponse("", err.Message(), err.Code()) + statusCode = err.Code() + } else { + response = responseDTO.NewMultiDeviceProfilesResponse("", "", http.StatusOK, deviceProfiles) + statusCode = http.StatusOK + } + } + + utils.WriteHttpHeader(w, ctx, statusCode) + pkg.Encode(response, w, lc) +} diff --git a/internal/core/metadata/v2/controller/http/deviceprofile_test.go b/internal/core/metadata/v2/controller/http/deviceprofile_test.go index bdefed935f..f799302251 100644 --- a/internal/core/metadata/v2/controller/http/deviceprofile_test.go +++ b/internal/core/metadata/v2/controller/http/deviceprofile_test.go @@ -987,3 +987,72 @@ func TestAllDeviceProfiles(t *testing.T) { }) } } + +func TestDeviceProfilesByModel(t *testing.T) { + deviceProfile := dtos.ToDeviceProfileModel(buildTestDeviceProfileRequest().Profile) + deviceProfiles := []models.DeviceProfile{deviceProfile, deviceProfile, deviceProfile} + + dic := mockDic() + dbClientMock := &dbMock.DBClient{} + dbClientMock.On("DeviceProfilesByModel", 0, 10, TestModel).Return(deviceProfiles, nil) + dbClientMock.On("DeviceProfilesByModel", 1, 2, TestModel).Return([]models.DeviceProfile{deviceProfiles[1], deviceProfiles[2]}, nil) + dbClientMock.On("DeviceProfilesByModel", 4, 1, TestModel).Return([]models.DeviceProfile{}, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, "query objects bounds out of range.", nil)) + dic.Update(di.ServiceConstructorMap{ + v2MetadataContainer.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + }) + controller := NewDeviceProfileController(dic) + assert.NotNil(t, controller) + + tests := []struct { + name string + offset string + limit string + model string + errorExpected bool + expectedCount int + expectedStatusCode int + }{ + {"Valid - get device profiles by model", "0", "10", TestModel, false, 3, http.StatusOK}, + {"Valid - get device profiles by model with offset and limit", "1", "2", TestModel, false, 2, http.StatusOK}, + {"Invalid - offset out of range", "4", "1", TestModel, true, 0, http.StatusNotFound}, + {"Invalid - model is empty", "0", "10", "", true, 0, http.StatusBadRequest}, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, contractsV2.ApiDeviceProfileByModelRoute, http.NoBody) + req = mux.SetURLVars(req, map[string]string{contractsV2.Model: testCase.model}) + query := req.URL.Query() + query.Add(contractsV2.Offset, testCase.offset) + query.Add(contractsV2.Limit, testCase.limit) + req.URL.RawQuery = query.Encode() + require.NoError(t, err) + + // Act + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(controller.DeviceProfilesByModel) + handler.ServeHTTP(recorder, req) + + // Assert + if testCase.errorExpected { + var res common.BaseResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + assert.Equal(t, contractsV2.ApiVersion, res.ApiVersion, "API Version not as expected") + assert.Equal(t, testCase.expectedStatusCode, recorder.Result().StatusCode, "HTTP status code not as expected") + assert.Equal(t, testCase.expectedStatusCode, int(res.StatusCode), "Response status code not as expected") + assert.NotEmpty(t, res.Message, "Response message doesn't contain the error message") + } else { + var res responseDTO.MultiDeviceProfilesResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + assert.Equal(t, contractsV2.ApiVersion, res.ApiVersion, "API Version not as expected") + assert.Equal(t, testCase.expectedStatusCode, recorder.Result().StatusCode, "HTTP status code not as expected") + assert.Equal(t, testCase.expectedStatusCode, int(res.StatusCode), "Response status code not as expected") + assert.Equal(t, testCase.expectedCount, len(res.Profiles), "Profile count not as expected") + assert.Empty(t, res.Message, "Message should be empty when it is successful") + } + }) + } +} diff --git a/internal/core/metadata/v2/infrastructure/interfaces/db.go b/internal/core/metadata/v2/infrastructure/interfaces/db.go index aed6b282de..22825dac20 100644 --- a/internal/core/metadata/v2/infrastructure/interfaces/db.go +++ b/internal/core/metadata/v2/infrastructure/interfaces/db.go @@ -20,6 +20,7 @@ type DBClient interface { DeleteDeviceProfileByName(name string) errors.EdgeX DeviceProfileNameExists(name string) (bool, errors.EdgeX) AllDeviceProfiles(offset int, limit int, labels []string) ([]model.DeviceProfile, errors.EdgeX) + DeviceProfilesByModel(offset int, limit int, model string) ([]model.DeviceProfile, errors.EdgeX) AddDeviceService(e model.DeviceService) (model.DeviceService, errors.EdgeX) DeviceServiceById(id string) (model.DeviceService, errors.EdgeX) diff --git a/internal/core/metadata/v2/infrastructure/interfaces/mocks/DBClient.go b/internal/core/metadata/v2/infrastructure/interfaces/mocks/DBClient.go index de8590be95..8e20ff98f3 100644 --- a/internal/core/metadata/v2/infrastructure/interfaces/mocks/DBClient.go +++ b/internal/core/metadata/v2/infrastructure/interfaces/mocks/DBClient.go @@ -398,6 +398,31 @@ func (_m *DBClient) DeviceProfileNameExists(name string) (bool, errors.EdgeX) { return r0, r1 } +// DeviceProfilesByModel provides a mock function with given fields: offset, limit, model +func (_m *DBClient) DeviceProfilesByModel(offset int, limit int, model string) ([]models.DeviceProfile, errors.EdgeX) { + ret := _m.Called(offset, limit, model) + + var r0 []models.DeviceProfile + if rf, ok := ret.Get(0).(func(int, int, string) []models.DeviceProfile); ok { + r0 = rf(offset, limit, model) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.DeviceProfile) + } + } + + var r1 errors.EdgeX + if rf, ok := ret.Get(1).(func(int, int, string) errors.EdgeX); ok { + r1 = rf(offset, limit, model) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + // DeviceServiceById provides a mock function with given fields: id func (_m *DBClient) DeviceServiceById(id string) (models.DeviceService, errors.EdgeX) { ret := _m.Called(id) diff --git a/internal/core/metadata/v2/router.go b/internal/core/metadata/v2/router.go index 4b55c2af39..26a45fd3b9 100644 --- a/internal/core/metadata/v2/router.go +++ b/internal/core/metadata/v2/router.go @@ -32,6 +32,7 @@ func LoadRestRoutes(r *mux.Router, dic *di.Container) { r.HandleFunc(v2Constant.ApiDeviceProfileByIdRoute, dc.DeleteDeviceProfileById).Methods(http.MethodDelete) r.HandleFunc(v2Constant.ApiDeviceProfileByNameRoute, dc.DeleteDeviceProfileByName).Methods(http.MethodDelete) r.HandleFunc(v2Constant.ApiAllDeviceProfileRoute, dc.AllDeviceProfiles).Methods(http.MethodGet) + r.HandleFunc(v2Constant.ApiDeviceProfileByModelRoute, dc.DeviceProfilesByModel).Methods(http.MethodGet) // Device Service ds := metadataController.NewDeviceServiceController(dic) diff --git a/internal/pkg/v2/infrastructure/redis/client.go b/internal/pkg/v2/infrastructure/redis/client.go index d033b3a853..44f13f4f02 100644 --- a/internal/pkg/v2/infrastructure/redis/client.go +++ b/internal/pkg/v2/infrastructure/redis/client.go @@ -241,6 +241,18 @@ func (c *Client) AllDeviceProfiles(offset int, limit int, labels []string) ([]mo return deviceProfiles, nil } +// DeviceProfilesByModel query device profiles with offset, limit and model +func (c *Client) DeviceProfilesByModel(offset int, limit int, model string) ([]model.DeviceProfile, errors.EdgeX) { + conn := c.Pool.Get() + defer conn.Close() + + deviceProfiles, edgeXerr := deviceProfilesByModel(conn, offset, limit, model) + if edgeXerr != nil { + return deviceProfiles, errors.NewCommonEdgeXWrapper(edgeXerr) + } + return deviceProfiles, nil +} + // EventTotalCount returns the total count of Event from the database func (c *Client) EventTotalCount() (uint32, errors.EdgeX) { conn := c.Pool.Get() diff --git a/internal/pkg/v2/infrastructure/redis/device_profile.go b/internal/pkg/v2/infrastructure/redis/device_profile.go index 559ca1a2ae..76512ed5b8 100644 --- a/internal/pkg/v2/infrastructure/redis/device_profile.go +++ b/internal/pkg/v2/infrastructure/redis/device_profile.go @@ -18,7 +18,10 @@ import ( "github.com/gomodule/redigo/redis" ) -const DeviceProfileCollection = "v2:deviceProfile" +const ( + DeviceProfileCollection = "v2:deviceProfile" + DeviceProfileCollectionModel = DeviceProfileCollection + ":" + v2.Model +) // deviceProfileStoredKey return the device profile's stored key which combines the collection name and object id func deviceProfileStoredKey(id string) string { @@ -79,7 +82,7 @@ func addDeviceProfile(conn redis.Conn, dp models.DeviceProfile) (addedDeviceProf _ = conn.Send(ZADD, DeviceProfileCollection, 0, storedKey) _ = conn.Send(HSET, fmt.Sprintf("%s:%s", DeviceProfileCollection, v2.Name), dp.Name, storedKey) _ = conn.Send(SADD, fmt.Sprintf("%s:%s:%s", DeviceProfileCollection, v2.Manufacturer, dp.Manufacturer), storedKey) - _ = conn.Send(SADD, fmt.Sprintf("%s:%s:%s", DeviceProfileCollection, v2.Model, dp.Model), storedKey) + _ = conn.Send(ZADD, fmt.Sprintf("%s:%s", DeviceProfileCollectionModel, dp.Model), dp.Modified, storedKey) for _, label := range dp.Labels { _ = conn.Send(ZADD, fmt.Sprintf("%s:%s:%s", DeviceProfileCollection, v2.Label, label), dp.Modified, storedKey) } @@ -209,3 +212,26 @@ func deviceProfilesByLabels(conn redis.Conn, offset int, limit int, labels []str } return deviceProfiles, nil } + +// deviceProfilesByModel query device profiles by offset, limit and model +func deviceProfilesByModel(conn redis.Conn, offset int, limit int, model string) (deviceProfiles []models.DeviceProfile, edgeXerr errors.EdgeX) { + end := offset + limit - 1 + if limit == -1 { //-1 limit means that clients want to retrieve all remaining records after offset from DB, so specifying -1 for end + end = limit + } + objects, err := getObjectsByRevRange(conn, fmt.Sprintf("%s:%s", DeviceProfileCollectionModel, model), offset, end) + if err != nil { + return deviceProfiles, errors.NewCommonEdgeXWrapper(err) + } + + deviceProfiles = make([]models.DeviceProfile, len(objects)) + for i, in := range objects { + dp := models.DeviceProfile{} + err := json.Unmarshal(in, &dp) + if err != nil { + return deviceProfiles, errors.NewCommonEdgeX(errors.KindContractInvalid, "device profile parsing failed", err) + } + deviceProfiles[i] = dp + } + return deviceProfiles, nil +} diff --git a/openapi/v2/core-data.yaml b/openapi/v2/core-data.yaml index 9d73436d34..0f4811a75e 100644 --- a/openapi/v2/core-data.yaml +++ b/openapi/v2/core-data.yaml @@ -256,9 +256,6 @@ components: - $ref: '#/components/schemas/BaseReading' - type: object properties: - floatEncoding: - description: "Indicates how a float value is encoded, if the value property contains a float. It should be 'Base64' or 'eNotation'" - type: string value: description: "A string representation of the reading's value" type: string @@ -388,7 +385,6 @@ components: modified: 1594975851631 origin: 1602168089665565300 pushed: 1594975851631 - floatEncoding: "eNotation" valueType: "Float32" value: "39.5" - apiVersion: "v2" @@ -436,7 +432,6 @@ components: labels: - "co2" origin: 1602168089665565300 - floatEncoding: "eNotation" valueType: "Float32" value: "12.2" AllReadingsExample: @@ -457,7 +452,6 @@ components: modified: 1594975851631 origin: 1602168089665565300 pushed: 1594975851631 - floatEncoding: "eNotation" valueType: "Float32" value: "39.5" - apiVersion: "v2" @@ -493,7 +487,6 @@ components: labels: - "co2" origin: 1602168089665565300 - floatEncoding: "eNotation" valueType: "Float32" value: "12.2" paths: @@ -523,7 +516,6 @@ paths: labels: - co2 origin: 1602168089665565300 - floatEncoding: eNotation valueType: Float32 value: '12.2' - event: @@ -538,7 +530,6 @@ paths: labels: - co2 origin: 1602168089665565300 - floatEncoding: eNotation valueType: Float32 value: '12.2' - event: @@ -689,7 +680,6 @@ paths: modified: 1594975851631 origin: 1602168089665565300 pushed: 1594975851631 - floatEncoding: "eNotation" valueType: "Float32" value: "39.5" - create: 1594876281221 @@ -987,7 +977,6 @@ paths: labels: - "co2" origin: 1602168089665565300 - floatEncoding: "eNotation" valueType: "Float32" value: "12.2" '404': @@ -1342,7 +1331,6 @@ paths: modified: 1594975851631 origin: 1602168089665565300 pushed: 1594975851631 - floatEncoding: "eNotation" valueType: "Float32" value: "39.5" '404': @@ -1409,7 +1397,6 @@ paths: modified: 1594975851631 origin: 1602168089665565300 pushed: 1594975851631 - floatEncoding: "eNotation" valueType: "Float32" value: "39.5" - apiVersion: "v2" @@ -1492,7 +1479,6 @@ paths: modified: 1594975851631 origin: 1602168089665565300 pushed: 1594975851631 - floatEncoding: "eNotation" valueType: "Float32" value: "39.5" - apiVersion: "v2" @@ -1504,7 +1490,6 @@ paths: labels: - "co2" origin: 1602168089665565300 - floatEncoding: "eNotation" valueType: "Float32" value: "12.2" '400': diff --git a/openapi/v2/core-metadata.yaml b/openapi/v2/core-metadata.yaml index 4a85992572..d15243fa40 100644 --- a/openapi/v2/core-metadata.yaml +++ b/openapi/v2/core-metadata.yaml @@ -652,9 +652,6 @@ components: assertion: type: string description: Required value of the property, set for checking error state. Failing an assertion condition will mark the device with an error state - floatEncoding: - type: string - description: FloatEncoding indicates the representation of floating value of reading. It should be 'Base64' or 'eNotation' mediaType: type: string description: A string value used to indicate the type of binary data if Type=binary @@ -1036,7 +1033,6 @@ components: type: "Float32" readWrite: "RW" defaultValue: "0" - floatEncoding: "Base64" deviceCommands: - name: "Float32" get: @@ -1072,7 +1068,6 @@ components: type: "Float32" readWrite: "RW" defaultValue: "0" - floatEncoding: "Base64" deviceCommands: - name: "Float32" get: @@ -2217,7 +2212,6 @@ paths: type: "Float32" readWrite: "RW" defaultValue: "0" - floatEncoding: "Base64" deviceCommands: - name: "Float32" get: @@ -2379,7 +2373,6 @@ paths: type: "Float32" readWrite: "RW" defaultValue: "0" - floatEncoding: "Base64" deviceCommands: - name: "Float32" get: