Skip to content

Commit

Permalink
feat(meta): Implement GET /device/profile/name/{name} V2 API (#2932)
Browse files Browse the repository at this point in the history
Implement GET  /device/profile/name/{name} V2 API according to the https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/core-metadata/2.x#/default/get_device_profile_name__name_

Fix #2931

Signed-off-by: weichou <[email protected]>
  • Loading branch information
weichou1229 authored Dec 8, 2020
1 parent 7f9dd25 commit 34dce46
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 0 deletions.
17 changes: 17 additions & 0 deletions internal/core/metadata/v2/application/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,20 @@ func DeviceByName(name string, dic *di.Container) (device dtos.Device, err error
device = dtos.FromDeviceModelToDTO(d)
return device, nil
}

// DevicesByProfileName query the devices with offset, limit, and profile name
func DevicesByProfileName(offset int, limit int, profileName string, dic *di.Container) (devices []dtos.Device, err errors.EdgeX) {
if profileName == "" {
return devices, errors.NewCommonEdgeX(errors.KindContractInvalid, "profileName is empty", nil)
}
dbClient := v2MetadataContainer.DBClientFrom(dic.Get)
deviceModels, err := dbClient.DevicesByProfileName(offset, limit, profileName)
if err != nil {
return devices, errors.NewCommonEdgeXWrapper(err)
}
devices = make([]dtos.Device, len(deviceModels))
for i, d := range deviceModels {
devices[i] = dtos.FromDeviceModelToDTO(d)
}
return devices, nil
}
38 changes: 38 additions & 0 deletions internal/core/metadata/v2/controller/http/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,41 @@ func (dc *DeviceController) DeviceByName(w http.ResponseWriter, r *http.Request)
utils.WriteHttpHeader(w, ctx, statusCode)
pkg.Encode(response, w, lc)
}

func (dc *DeviceController) DevicesByProfileName(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)

var response interface{}
var statusCode int

vars := mux.Vars(r)
name := vars[v2.Name]

// 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 {
devices, err := application.DevicesByProfileName(offset, limit, name, 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.NewMultiDevicesResponse("", "", http.StatusOK, devices)
statusCode = http.StatusOK
}
}

utils.WriteHttpHeader(w, ctx, statusCode)
pkg.Encode(response, w, lc)
}
78 changes: 78 additions & 0 deletions internal/core/metadata/v2/controller/http/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,3 +801,81 @@ func TestDeviceByName(t *testing.T) {
})
}
}

func TestDevicesByProfileName(t *testing.T) {
device := dtos.ToDeviceModel(buildTestDeviceRequest().Device)
testProfileA := "testProfileA"
testProfileB := "testServiceB"
device1WithProfileA := device
device1WithProfileA.ProfileName = testProfileA
device2WithProfileA := device
device2WithProfileA.ProfileName = testProfileA
device3WithProfileB := device
device3WithProfileB.ProfileName = testProfileB

devices := []models.Device{device1WithProfileA, device2WithProfileA, device3WithProfileB}

dic := mockDic()
dbClientMock := &dbMock.DBClient{}
dbClientMock.On("DevicesByProfileName", 0, 5, testProfileA).Return([]models.Device{devices[0], devices[1]}, nil)
dbClientMock.On("DevicesByProfileName", 1, 1, testProfileA).Return([]models.Device{devices[1]}, nil)
dbClientMock.On("DevicesByProfileName", 4, 1, testProfileB).Return([]models.Device{}, 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 := NewDeviceController(dic)
assert.NotNil(t, controller)

tests := []struct {
name string
offset string
limit string
profileName string
errorExpected bool
expectedCount int
expectedStatusCode int
}{
{"Valid - get devices with profileName", "0", "5", testProfileA, false, 2, http.StatusOK},
{"Valid - get devices with offset and limit", "1", "1", testProfileA, false, 1, http.StatusOK},
{"Invalid - offset out of range", "4", "1", testProfileB, true, 0, http.StatusNotFound},
{"Invalid - get devices without profileName", "0", "10", "", true, 0, http.StatusBadRequest},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, v2.ApiDeviceByProfileNameRoute, http.NoBody)
query := req.URL.Query()
query.Add(v2.Offset, testCase.offset)
query.Add(v2.Limit, testCase.limit)
req.URL.RawQuery = query.Encode()
req = mux.SetURLVars(req, map[string]string{v2.Name: testCase.profileName})
require.NoError(t, err)

// Act
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(controller.DevicesByProfileName)
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, v2.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.MultiDevicesResponse
err = json.Unmarshal(recorder.Body.Bytes(), &res)
require.NoError(t, err)
assert.Equal(t, v2.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.Devices), "Device count not as expected")
assert.Empty(t, res.Message, "Message should be empty when it is successful")
}
})
}
}
1 change: 1 addition & 0 deletions internal/core/metadata/v2/infrastructure/interfaces/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ type DBClient interface {
DeviceById(id string) (model.Device, errors.EdgeX)
DeviceByName(name string) (model.Device, errors.EdgeX)
AllDevices(offset int, limit int, labels []string) ([]model.Device, errors.EdgeX)
DevicesByProfileName(offset int, limit int, profileName string) ([]model.Device, errors.EdgeX)
}

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

1 change: 1 addition & 0 deletions internal/core/metadata/v2/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func LoadRestRoutes(r *mux.Router, dic *di.Container) {
r.HandleFunc(v2Constant.ApiDeviceRoute, d.PatchDevice).Methods(http.MethodPatch)
r.HandleFunc(v2Constant.ApiAllDeviceRoute, d.AllDevices).Methods(http.MethodGet)
r.HandleFunc(v2Constant.ApiDeviceByNameRoute, d.DeviceByName).Methods(http.MethodGet)
r.HandleFunc(v2Constant.ApiDeviceByProfileNameRoute, d.DevicesByProfileName).Methods(http.MethodGet)

r.Use(correlation.ManageHeader)
r.Use(correlation.OnResponseComplete)
Expand Down
13 changes: 13 additions & 0 deletions internal/pkg/v2/infrastructure/redis/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,19 @@ func (c *Client) DeviceByName(name string) (device model.Device, edgeXerr errors
return
}

// DevicesByProfileName query devices by offset, limit and profile name
func (c *Client) DevicesByProfileName(offset int, limit int, profileName string) (devices []model.Device, edgeXerr errors.EdgeX) {
conn := c.Pool.Get()
defer conn.Close()

devices, edgeXerr = devicesByProfileName(conn, offset, limit, profileName)
if edgeXerr != nil {
return devices, errors.NewCommonEdgeX(errors.Kind(edgeXerr),
fmt.Sprintf("fail to query devices by offset %d, limit %d and name %s", offset, limit, profileName), edgeXerr)
}
return devices, nil
}

// AllEvents query events by offset and limit
func (c *Client) AllEvents(offset int, limit int) ([]model.Event, errors.EdgeX) {
conn := c.Pool.Get()
Expand Down
26 changes: 26 additions & 0 deletions internal/pkg/v2/infrastructure/redis/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
DeviceCollectionName = DeviceCollection + DBKeySeparator + v2.Name
DeviceCollectionLabel = DeviceCollection + DBKeySeparator + v2.Label
DeviceCollectionServiceName = DeviceCollection + DBKeySeparator + v2.Service + DBKeySeparator + v2.Name
DeviceCollectionProfileName = DeviceCollection + DBKeySeparator + v2.Profile + DBKeySeparator + v2.Name
)

// deviceStoredKey return the device's stored key which combines the collection name and object id
Expand Down Expand Up @@ -81,6 +82,7 @@ func addDevice(conn redis.Conn, d models.Device) (models.Device, errors.EdgeX) {
_ = conn.Send(ZADD, DeviceCollection, 0, storedKey)
_ = conn.Send(HSET, DeviceCollectionName, d.Name, storedKey)
_ = conn.Send(ZADD, CreateKey(DeviceCollectionServiceName, d.ServiceName), d.Modified, storedKey)
_ = conn.Send(ZADD, CreateKey(DeviceCollectionProfileName, d.ProfileName), d.Modified, storedKey)
for _, label := range d.Labels {
_ = conn.Send(ZADD, CreateKey(DeviceCollectionLabel, label), d.Modified, storedKey)
}
Expand Down Expand Up @@ -144,6 +146,7 @@ func deleteDevice(conn redis.Conn, device models.Device) errors.EdgeX {
_ = conn.Send(ZREM, DeviceCollection, storedKey)
_ = conn.Send(HDEL, DeviceCollectionName, device.Name)
_ = conn.Send(ZREM, CreateKey(DeviceCollectionServiceName, device.ServiceName), storedKey)
_ = conn.Send(ZREM, CreateKey(DeviceCollectionProfileName, device.ProfileName), storedKey)
for _, label := range device.Labels {
_ = conn.Send(ZREM, CreateKey(DeviceCollectionLabel, label), storedKey)
}
Expand Down Expand Up @@ -199,3 +202,26 @@ func devicesByLabels(conn redis.Conn, offset int, limit int, labels []string) (d
}
return devices, nil
}

// devicesByProfileName query devices by offset, limit and profile name
func devicesByProfileName(conn redis.Conn, offset int, limit int, profileName string) (devices []models.Device, 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, CreateKey(DeviceCollectionProfileName, profileName), offset, end)
if err != nil {
return devices, errors.NewCommonEdgeXWrapper(err)
}

devices = make([]models.Device, len(objects))
for i, in := range objects {
s := models.Device{}
err := json.Unmarshal(in, &s)
if err != nil {
return []models.Device{}, errors.NewCommonEdgeX(errors.KindDatabaseError, "device format parsing failed from the database", err)
}
devices[i] = s
}
return devices, nil
}

0 comments on commit 34dce46

Please sign in to comment.