modelRT/handler/data_object_attribute_query.go

375 lines
11 KiB
Go
Raw Normal View History

// Package handler provides HTTP handlers for various endpoints.
package handler
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"modelRT/common"
"modelRT/common/errcode"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/model"
"modelRT/orm"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
// DataObjectAttributeQueryHandler define data object attribute value query process API
func DataObjectAttributeQueryHandler(c *gin.Context) {
ctx := c.Request.Context()
token, field, err := parseDataObjectAttributeQuery(c)
if err != nil {
logger.Error(ctx, "query token from query parameters failed", "error", err, "url", c.Request.RequestURI)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
dataObjectType, err := model.ClassifyDataObjectToken(token)
if err != nil {
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
if err := validateDataObjectField(dataObjectType, field); err != nil {
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
value, err := queryDataObjectAttributeValue(
ctx,
dataObjectType,
token,
field,
loadDataObjectHashField,
loadMeasurementValueMetadata,
queryMeasurementRealtimeValue,
)
if err != nil {
if isDataObjectTokenNotFound(err) {
logger.Warn(ctx, "query data-object token from redis failed", "token", token, "field", field, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
logger.Error(ctx, "query data-object attribute from redis failed", "token", token, "field", field, "error", err)
renderRespFailure(c, constants.RespCodeServerError, dataObjectAttributeFailureMessage(dataObjectType), nil)
return
}
result := dataObjectAttributeQueryResult{
Token: token,
Field: field,
Code: errcode.ErrProcessSuccess.Code(),
Msg: errcode.ErrProcessSuccess.Msg(),
Value: value,
}
renderRespSuccess(c, constants.RespCodeSuccess, dataObjectAttributeSuccessMessage(dataObjectType), map[string]any{
"attributes": []dataObjectAttributeQueryResult{result},
})
}
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 dataObjectHashFieldLoader func(context.Context, constants.DataObjectType, string, string) (string, error)
type measurementValueMetadataLoader func(context.Context, string) (orm.JSONMap, int, error)
type measurementValueLoader func(context.Context, orm.JSONMap, int) (any, error)
var measurementDataObjectFields = map[string]struct{}{
"value": {},
"mode": {},
"meta": {},
"type": {},
"name": {},
"description": {},
"id": {},
"size": {},
"data_source": {},
"event_plan": {},
"binding": {},
}
var parameterDataObjectFields = map[string]struct{}{
"value": {},
"name": {},
"meta": {},
"type": {},
"description": {},
"id": {},
}
type dataObjectAttributeQueryResult struct {
Token string `json:"token"`
Field string `json:"field"`
Code int `json:"code"`
Msg string `json:"msg"`
Value any `json:"value"`
}
func validateDataObjectField(dataObjectType constants.DataObjectType, field string) error {
field = strings.ToLower(field)
switch dataObjectType {
case constants.DataObjectTypeMeasurement:
if _, ok := measurementDataObjectFields[field]; ok {
return nil
}
return fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
case constants.DataObjectTypeParameter:
if _, ok := parameterDataObjectFields[field]; ok {
return nil
}
return fmt.Errorf("%w: %s", common.ErrUnsupportedParameterField, field)
default:
return fmt.Errorf("invalid data object type %q", dataObjectType)
}
}
func queryDataObjectAttributeValue(
ctx context.Context,
dataObjectType constants.DataObjectType,
token string,
field string,
loadHashField dataObjectHashFieldLoader,
loadMeasurementMetadata measurementValueMetadataLoader,
loadMeasurementValue measurementValueLoader,
) (any, error) {
if dataObjectType == constants.DataObjectTypeMeasurement && field == "value" {
if loadMeasurementMetadata == nil {
return nil, fmt.Errorf("measurement value metadata loader is nil")
}
if loadMeasurementValue == nil {
return nil, fmt.Errorf("measurement value loader is nil")
}
dataSource, size, err := loadMeasurementMetadata(ctx, token)
if err != nil {
return nil, err
}
return loadMeasurementValue(ctx, dataSource, size)
}
if loadHashField == nil {
return nil, fmt.Errorf("data-object hash field loader is nil")
}
rawValue, err := loadHashField(ctx, dataObjectType, token, field)
if err != nil {
return nil, err
}
if dataObjectType == constants.DataObjectTypeParameter && field == "value" {
attributeType, err := loadHashField(ctx, dataObjectType, token, "type")
if err != nil {
return nil, err
}
return decodeParameterHashValue(rawValue, attributeType)
}
return decodeDataObjectHashField(dataObjectType, field, rawValue)
}
func loadDataObjectHashField(
ctx context.Context,
dataObjectType constants.DataObjectType,
token string,
field string,
) (string, error) {
rdb := diagram.GetRedisClientInstance()
if rdb == nil {
return "", fmt.Errorf("redis client is not initialized")
}
canonicalKey, err := model.ResolveDataObjectRedisKey(ctx, rdb, dataObjectType, token)
if err != nil {
return "", err
}
value, err := rdb.HGet(ctx, canonicalKey, field).Result()
if errors.Is(err, redis.Nil) {
return "", fmt.Errorf("canonical redis data-object hash %q does not contain field %q", canonicalKey, field)
}
if err != nil {
return "", fmt.Errorf("query canonical redis hash %q field %q: %w", canonicalKey, field, err)
}
return value, nil
}
func loadMeasurementValueMetadata(ctx context.Context, token string) (orm.JSONMap, int, error) {
rdb := diagram.GetRedisClientInstance()
if rdb == nil {
return nil, 0, fmt.Errorf("redis client is not initialized")
}
canonicalKey, err := model.ResolveDataObjectRedisKey(ctx, rdb, constants.DataObjectTypeMeasurement, token)
if err != nil {
return nil, 0, err
}
values, err := rdb.HMGet(ctx, canonicalKey, "data_source", "size").Result()
if err != nil {
return nil, 0, fmt.Errorf("query canonical redis hash %q measurement value metadata: %w", canonicalKey, err)
}
if len(values) != 2 {
return nil, 0, fmt.Errorf("canonical redis hash %q returned %d measurement metadata fields", canonicalKey, len(values))
}
if values[0] == nil || values[1] == nil {
missingFields := make([]string, 0, 2)
if values[0] == nil {
missingFields = append(missingFields, "data_source")
}
if values[1] == nil {
missingFields = append(missingFields, "size")
}
return nil, 0, fmt.Errorf(
"canonical redis measurement hash %q does not contain field(s) %s",
canonicalKey,
strings.Join(missingFields, ", "),
)
}
rawDataSource, ok := values[0].(string)
if !ok {
return nil, 0, fmt.Errorf("canonical redis measurement hash %q data_source has type %T", canonicalKey, values[0])
}
var dataSource orm.JSONMap
if err := json.Unmarshal([]byte(rawDataSource), &dataSource); err != nil {
return nil, 0, fmt.Errorf("decode measurement data_source from canonical redis hash %q: %w", canonicalKey, err)
}
rawSize, ok := values[1].(string)
if !ok {
return nil, 0, fmt.Errorf("canonical redis measurement hash %q size has type %T", canonicalKey, values[1])
}
size, err := strconv.Atoi(rawSize)
if err != nil {
return nil, 0, fmt.Errorf("decode measurement size %q: %w", rawSize, err)
}
if size <= 0 {
return nil, 0, fmt.Errorf("measurement window size must be greater than 0, got %d", size)
}
return dataSource, size, nil
}
func decodeDataObjectHashField(
dataObjectType constants.DataObjectType,
field string,
rawValue string,
) (any, error) {
if dataObjectType != constants.DataObjectTypeMeasurement {
return rawValue, nil
}
switch field {
case "mode":
value, err := strconv.ParseInt(rawValue, 10, 16)
if err != nil {
return nil, fmt.Errorf("decode measurement mode %q: %w", rawValue, err)
}
return int16(value), nil
case "size":
value, err := strconv.Atoi(rawValue)
if err != nil {
return nil, fmt.Errorf("decode measurement size %q: %w", rawValue, err)
}
return value, nil
case "data_source", "event_plan", "binding":
return decodeRedisJSON(rawValue)
default:
return rawValue, nil
}
}
func decodeParameterHashValue(rawValue, attributeType string) (any, error) {
normalizedType := strings.ToUpper(strings.TrimSpace(attributeType))
if rawValue == "null" {
return nil, nil
}
switch {
case normalizedType == "BOOLEAN":
value, err := strconv.ParseBool(rawValue)
if err != nil {
return nil, fmt.Errorf("decode parameter boolean value %q: %w", rawValue, err)
}
return value, nil
case normalizedType == "SMALLINT",
normalizedType == "INTEGER",
normalizedType == "BIGINT":
value, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil {
return nil, fmt.Errorf("decode parameter integer value %q: %w", rawValue, err)
}
return value, nil
case normalizedType == "REAL",
normalizedType == "DOUBLE PRECISION",
strings.HasPrefix(normalizedType, "NUMERIC"),
strings.HasPrefix(normalizedType, "DECIMAL"):
if _, err := strconv.ParseFloat(rawValue, 64); err != nil {
return nil, fmt.Errorf("decode parameter numeric value %q: %w", rawValue, err)
}
return json.Number(rawValue), nil
case normalizedType == "JSON",
normalizedType == "JSONB",
strings.HasSuffix(normalizedType, "[]"):
return decodeRedisJSON(rawValue)
default:
return rawValue, nil
}
}
func decodeRedisJSON(rawValue string) (any, error) {
var value any
decoder := json.NewDecoder(strings.NewReader(rawValue))
decoder.UseNumber()
if err := decoder.Decode(&value); err != nil {
return nil, fmt.Errorf("decode redis JSON value: %w", err)
}
return value, nil
}
func isDataObjectTokenNotFound(err error) bool {
return errors.Is(err, common.ErrParameterTokenNotFound) ||
errors.Is(err, common.ErrMeasurementTokenNotFound)
}
func dataObjectAttributeSuccessMessage(dataObjectType constants.DataObjectType) string {
if dataObjectType == constants.DataObjectTypeParameter {
return "query parameter attribute success"
}
return "query measurement attribute success"
}
func dataObjectAttributeFailureMessage(dataObjectType constants.DataObjectType) string {
if dataObjectType == constants.DataObjectTypeParameter {
return "query parameter attribute failed"
}
return "query measurement attribute failed"
}
func queryMeasurementRealtimeValue(ctx context.Context, dataSource orm.JSONMap, size int) (any, error) {
queryKey, err := model.GenerateMeasureIdentifier(dataSource)
if err != nil {
return nil, fmt.Errorf("generate measurement redis key: %w", err)
}
value, err := diagram.NewRedisClient().QueryLatestMeasurementValues(ctx, queryKey, size)
if err != nil {
return nil, fmt.Errorf("query real-time measurement value by key %q: %w", queryKey, err)
}
return value, nil
}