// 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 }