refactor: use query parameters for data-object attribute APIs
- read token and field from query parameters - default missing or empty fields to value - expose attribute query and update through GET and PATCH routes - return true only for measurement mode 1 - add query parsing and measurement mode test coverage
This commit is contained in:
parent
38f21f3cda
commit
ab029e292b
|
|
@ -24,16 +24,9 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
token := c.Param("token")
|
token, field, err := parseDataObjectAttributeQuery(c)
|
||||||
field := c.Param("field")
|
if err != nil {
|
||||||
if token == "" {
|
logger.Error(ctx, "query token from query parameters failed", "error", err, "url", c.Request.RequestURI)
|
||||||
err := fmt.Errorf("tokens is missing from the path")
|
|
||||||
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
} else if field == "" {
|
|
||||||
err := fmt.Errorf("field is missing from the path")
|
|
||||||
logger.Error(ctx, "query field from path failed", "error", err, "url", c.Request.RequestURI)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -45,8 +38,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedField := strings.ToLower(field)
|
if err := validateDataObjectField(token, dataObjectType, field); err != nil {
|
||||||
if err := validateDataObjectField(token, dataObjectType, normalizedField); err != nil {
|
|
||||||
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
|
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
|
|
@ -94,7 +86,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
case constants.DataObjectTypeParameter:
|
case constants.DataObjectTypeParameter:
|
||||||
value, err := buildParameterAttributeValue(
|
value, err := buildParameterAttributeValue(
|
||||||
ctx,
|
ctx,
|
||||||
normalizedField,
|
field,
|
||||||
parameter,
|
parameter,
|
||||||
func(ctx context.Context, parameter *database.ParameterDataObject) (any, error) {
|
func(ctx context.Context, parameter *database.ParameterDataObject) (any, error) {
|
||||||
return database.QueryParameterDataObjectValue(ctx, pgClient, parameter)
|
return database.QueryParameterDataObjectValue(ctx, pgClient, parameter)
|
||||||
|
|
@ -117,7 +109,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
|
|
||||||
result := dataObjectAttributeQueryResult{
|
result := dataObjectAttributeQueryResult{
|
||||||
Token: token,
|
Token: token,
|
||||||
Field: normalizedField,
|
Field: field,
|
||||||
Code: errcode.ErrProcessSuccess.Code(),
|
Code: errcode.ErrProcessSuccess.Code(),
|
||||||
Msg: errcode.ErrProcessSuccess.Msg(),
|
Msg: errcode.ErrProcessSuccess.Msg(),
|
||||||
Value: value,
|
Value: value,
|
||||||
|
|
@ -128,7 +120,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
case constants.DataObjectTypeMeasurement:
|
case constants.DataObjectTypeMeasurement:
|
||||||
value, err := buildMeasurementAttributeValue(
|
value, err := buildMeasurementAttributeValue(
|
||||||
ctx,
|
ctx,
|
||||||
normalizedField,
|
field,
|
||||||
measurement,
|
measurement,
|
||||||
measurementComponent,
|
measurementComponent,
|
||||||
queryMeasurementRealtimeValue,
|
queryMeasurementRealtimeValue,
|
||||||
|
|
@ -147,7 +139,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
|
|
||||||
result := dataObjectAttributeQueryResult{
|
result := dataObjectAttributeQueryResult{
|
||||||
Token: token,
|
Token: token,
|
||||||
Field: normalizedField,
|
Field: field,
|
||||||
Code: errcode.ErrProcessSuccess.Code(),
|
Code: errcode.ErrProcessSuccess.Code(),
|
||||||
Msg: errcode.ErrProcessSuccess.Msg(),
|
Msg: errcode.ErrProcessSuccess.Msg(),
|
||||||
Value: value,
|
Value: value,
|
||||||
|
|
@ -160,6 +152,19 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseDataObjectAttributeQuery(c *gin.Context) (string, string, error) {
|
||||||
|
token := c.Query("token")
|
||||||
|
if token == "" {
|
||||||
|
return "", "", fmt.Errorf("token is missing from query parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
field := strings.ToLower(c.Query("field"))
|
||||||
|
if field == "" {
|
||||||
|
field = "value"
|
||||||
|
}
|
||||||
|
return token, field, nil
|
||||||
|
}
|
||||||
|
|
||||||
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
|
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
|
||||||
|
|
||||||
type parameterValueLoader func(context.Context, *database.ParameterDataObject) (any, error)
|
type parameterValueLoader func(context.Context, *database.ParameterDataObject) (any, error)
|
||||||
|
|
@ -285,15 +290,7 @@ func buildMeasurementAttributeValue(
|
||||||
}
|
}
|
||||||
return loadValue(ctx, measurement.DataSource)
|
return loadValue(ctx, measurement.DataSource)
|
||||||
case "mode":
|
case "mode":
|
||||||
fmt.Println("measurement.Mode:", measurement.Mode)
|
return measurement.Mode == 1, nil
|
||||||
switch measurement.Mode {
|
|
||||||
case 0:
|
|
||||||
return false, nil
|
|
||||||
case 1:
|
|
||||||
return true, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid measurement mode %d: expected 0 or 1", measurement.Mode)
|
|
||||||
}
|
|
||||||
case "meta":
|
case "meta":
|
||||||
return "MEASUREMENT", nil
|
return "MEASUREMENT", nil
|
||||||
case "type":
|
case "type":
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"modelRT/common"
|
"modelRT/common"
|
||||||
|
|
@ -9,10 +11,75 @@ import (
|
||||||
"modelRT/database"
|
"modelRT/database"
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestParseDataObjectAttributeQuery(t *testing.T) {
|
||||||
|
const token = "nspath.component.bay.measurement"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
target string
|
||||||
|
wantToken string
|
||||||
|
wantField string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "reads token and field from query parameters",
|
||||||
|
target: "/data-object/attribute?token=" + token + "&field=NAME",
|
||||||
|
wantToken: token,
|
||||||
|
wantField: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaults missing field to value",
|
||||||
|
target: "/data-object/attribute?token=" + token,
|
||||||
|
wantToken: token,
|
||||||
|
wantField: "value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaults empty field to value",
|
||||||
|
target: "/data-object/attribute?token=" + token + "&field=",
|
||||||
|
wantToken: token,
|
||||||
|
wantField: "value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rejects missing token",
|
||||||
|
target: "/data-object/attribute?field=value",
|
||||||
|
wantErr: "token is missing from query parameters",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rejects empty token",
|
||||||
|
target: "/data-object/attribute?token=&field=value",
|
||||||
|
wantErr: "token is missing from query parameters",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not read legacy path parameters",
|
||||||
|
target: "/data-object/attribute/" + token + "/value",
|
||||||
|
wantErr: "token is missing from query parameters",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodGet, tt.target, nil)
|
||||||
|
|
||||||
|
token, field, err := parseDataObjectAttributeQuery(ctx)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
assert.Empty(t, token)
|
||||||
|
assert.Empty(t, field)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantToken, token)
|
||||||
|
assert.Equal(t, tt.wantField, field)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateDataObjectField(t *testing.T) {
|
func TestValidateDataObjectField(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -182,34 +249,28 @@ func TestBuildMeasurementAttributeValueRejectsUnsupportedField(t *testing.T) {
|
||||||
|
|
||||||
func TestBuildMeasurementAttributeValueMode(t *testing.T) {
|
func TestBuildMeasurementAttributeValueMode(t *testing.T) {
|
||||||
component := &orm.Component{}
|
component := &orm.Component{}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode int16
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{name: "automatic mode", mode: 1, expected: true},
|
||||||
|
{name: "manual mode", mode: 0, expected: false},
|
||||||
|
{name: "other positive mode", mode: 2, expected: false},
|
||||||
|
{name: "negative mode", mode: -1, expected: false},
|
||||||
|
}
|
||||||
|
|
||||||
manualMode, err := buildMeasurementAttributeValue(
|
for _, tt := range tests {
|
||||||
context.Background(),
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
"mode",
|
actual, err := buildMeasurementAttributeValue(
|
||||||
&orm.Measurement{Mode: 0},
|
context.Background(),
|
||||||
component,
|
"mode",
|
||||||
nil,
|
&orm.Measurement{Mode: tt.mode},
|
||||||
)
|
component,
|
||||||
require.NoError(t, err)
|
nil,
|
||||||
assert.Equal(t, false, manualMode)
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
automaticMode, err := buildMeasurementAttributeValue(
|
assert.Equal(t, tt.expected, actual)
|
||||||
context.Background(),
|
})
|
||||||
"mode",
|
}
|
||||||
&orm.Measurement{Mode: 1},
|
|
||||||
component,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, true, automaticMode)
|
|
||||||
|
|
||||||
_, err = buildMeasurementAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
"mode",
|
|
||||||
&orm.Measurement{Mode: 2},
|
|
||||||
component,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "expected 0 or 1")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ import (
|
||||||
func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFunc) {
|
func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFunc) {
|
||||||
g := rg.Group("/data-object/")
|
g := rg.Group("/data-object/")
|
||||||
g.Use(middlewares...)
|
g.Use(middlewares...)
|
||||||
g.GET("attribute/:token/:field", handler.DataObjectAttributeQueryHandler)
|
g.GET("attribute", handler.DataObjectAttributeQueryHandler)
|
||||||
g.POST("update", handler.DataObjectAttributeUpdateHandler)
|
g.PATCH("attribute", handler.DataObjectAttributeUpdateHandler)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue