mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-28 13:48:49 +08:00

* NOISSUE - Add timestamp transformation rules for specifc JSON fields Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add tests and defaults Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix reviews Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix Created Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix READMEs Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix Config file Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Improve the timestamp Config structure Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix typos Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix smpp Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix reviews Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Rm duplicated Unmarshal Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Extract the error var Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix review Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Return error if time field transformation fails Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
197 lines
4.8 KiB
Go
197 lines
4.8 KiB
Go
// Copyright (c) Mainflux
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package json
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/mainflux/mainflux/pkg/errors"
|
|
"github.com/mainflux/mainflux/pkg/messaging"
|
|
"github.com/mainflux/mainflux/pkg/transformers"
|
|
)
|
|
|
|
const sep = "/"
|
|
|
|
var (
|
|
keys = [...]string{"publisher", "protocol", "channel", "subtopic"}
|
|
|
|
// ErrTransform represents an error during parsing message.
|
|
ErrTransform = errors.New("unable to parse JSON object")
|
|
// ErrInvalidKey represents the use of a reserved message field.
|
|
ErrInvalidKey = errors.New("invalid object key")
|
|
// ErrInvalidTimeField represents the use an invalid time field.
|
|
ErrInvalidTimeField = errors.New("invalid time field")
|
|
|
|
errUnknownFormat = errors.New("unknown format of JSON message")
|
|
errInvalidFormat = errors.New("invalid JSON object")
|
|
errInvalidNestedJSON = errors.New("invalid nested JSON object")
|
|
)
|
|
|
|
// TimeField represents the message fields to use as timestamp
|
|
type TimeField struct {
|
|
FieldName string `toml:"field_name"`
|
|
FieldFormat string `toml:"field_format"`
|
|
Location string `toml:"location"`
|
|
}
|
|
|
|
type transformerService struct {
|
|
timeFields []TimeField
|
|
}
|
|
|
|
// New returns a new JSON transformer.
|
|
func New(tfs []TimeField) transformers.Transformer {
|
|
return &transformerService{
|
|
timeFields: tfs,
|
|
}
|
|
}
|
|
|
|
// Transform transforms Mainflux message to a list of JSON messages.
|
|
func (ts *transformerService) Transform(msg messaging.Message) (interface{}, error) {
|
|
ret := Message{
|
|
Publisher: msg.Publisher,
|
|
Created: msg.Created,
|
|
Protocol: msg.Protocol,
|
|
Channel: msg.Channel,
|
|
Subtopic: msg.Subtopic,
|
|
}
|
|
|
|
if ret.Subtopic == "" {
|
|
return nil, errors.Wrap(ErrTransform, errUnknownFormat)
|
|
}
|
|
|
|
subs := strings.Split(ret.Subtopic, ".")
|
|
if len(subs) == 0 {
|
|
return nil, errors.Wrap(ErrTransform, errUnknownFormat)
|
|
}
|
|
|
|
format := subs[len(subs)-1]
|
|
var payload interface{}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return nil, errors.Wrap(ErrTransform, err)
|
|
}
|
|
|
|
switch p := payload.(type) {
|
|
case map[string]interface{}:
|
|
ret.Payload = p
|
|
|
|
// Apply timestamp transformation rules depending on key/unit pairs
|
|
ts, err := ts.transformTimeField(p)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ErrInvalidTimeField, err)
|
|
}
|
|
if ts != 0 {
|
|
ret.Created = ts
|
|
}
|
|
|
|
return Messages{[]Message{ret}, format}, nil
|
|
case []interface{}:
|
|
res := []Message{}
|
|
// Make an array of messages from the root array.
|
|
for _, val := range p {
|
|
v, ok := val.(map[string]interface{})
|
|
if !ok {
|
|
return nil, errors.Wrap(ErrTransform, errInvalidNestedJSON)
|
|
}
|
|
newMsg := ret
|
|
|
|
// Apply timestamp transformation rules depending on key/unit pairs
|
|
ts, err := ts.transformTimeField(v)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ErrInvalidTimeField, err)
|
|
}
|
|
if ts != 0 {
|
|
ret.Created = ts
|
|
}
|
|
|
|
newMsg.Payload = v
|
|
res = append(res, newMsg)
|
|
}
|
|
return Messages{res, format}, nil
|
|
default:
|
|
return nil, errors.Wrap(ErrTransform, errInvalidFormat)
|
|
}
|
|
}
|
|
|
|
// ParseFlat receives flat map that represents complex JSON objects and returns
|
|
// the corresponding complex JSON object with nested maps. It's the opposite
|
|
// of the Flatten function.
|
|
func ParseFlat(flat interface{}) interface{} {
|
|
msg := make(map[string]interface{})
|
|
switch v := flat.(type) {
|
|
case map[string]interface{}:
|
|
for key, value := range v {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
subKeys := strings.Split(key, sep)
|
|
n := len(subKeys)
|
|
if n == 1 {
|
|
msg[key] = value
|
|
continue
|
|
}
|
|
current := msg
|
|
for i, k := range subKeys {
|
|
if _, ok := current[k]; !ok {
|
|
current[k] = make(map[string]interface{})
|
|
}
|
|
if i == n-1 {
|
|
current[k] = value
|
|
break
|
|
}
|
|
current = current[k].(map[string]interface{})
|
|
}
|
|
}
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// Flatten makes nested maps flat using composite keys created by concatenation of the nested keys.
|
|
func Flatten(m map[string]interface{}) (map[string]interface{}, error) {
|
|
return flatten("", make(map[string]interface{}), m)
|
|
}
|
|
|
|
func flatten(prefix string, m, m1 map[string]interface{}) (map[string]interface{}, error) {
|
|
for k, v := range m1 {
|
|
if strings.Contains(k, sep) {
|
|
return nil, ErrInvalidKey
|
|
}
|
|
for _, key := range keys {
|
|
if k == key {
|
|
return nil, ErrInvalidKey
|
|
}
|
|
}
|
|
switch val := v.(type) {
|
|
case map[string]interface{}:
|
|
var err error
|
|
m, err = flatten(prefix+k+sep, m, val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
m[prefix+k] = v
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (ts *transformerService) transformTimeField(payload map[string]interface{}) (int64, error) {
|
|
if len(ts.timeFields) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
for _, tf := range ts.timeFields {
|
|
if val, ok := payload[tf.FieldName]; ok {
|
|
t, err := parseTimestamp(tf.FieldFormat, val, tf.Location)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return t.UnixNano(), nil
|
|
}
|
|
}
|
|
|
|
return 0, nil
|
|
}
|