File size: 4,847 Bytes
d6f631f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package meter

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"strconv"

	"github.com/oliveagle/jsonpath"
	"github.com/samber/lo"

	"github.com/openmeterio/openmeter/pkg/models"
)

var _ models.GenericError = (*ErrInvalidEvent)(nil)

type ErrInvalidEvent struct {
	err error
}

func (e ErrInvalidEvent) Error() string {
	s := "invalid event"
	if e.err != nil {
		return s + ": " + e.err.Error()
	}

	return s
}

func (e ErrInvalidEvent) Unwrap() error {
	return e.err
}

func NewErrInvalidEvent(err error) error {
	return ErrInvalidEvent{err: err}
}

var _ models.GenericError = (*ErrInvalidMeter)(nil)

type ErrInvalidMeter struct {
	err error
}

func (e ErrInvalidMeter) Error() string {
	s := "invalid meter"
	if e.err != nil {
		return s + ": " + e.err.Error()
	}

	return s
}

func (e ErrInvalidMeter) Unwrap() error {
	return e.err
}

func NewErrInvalidMeter(err error) error {
	return &ErrInvalidMeter{err: err}
}

type ParsedEvent struct {
	Value       *float64
	ValueString *string
	GroupBy     map[string]string
}

func ParseEventString(meter Meter, data string) (*ParsedEvent, error) {
	return ParseEvent(meter, []byte(data))
}

// ParseEvent validates and parses an event against a meter.
func ParseEvent(meter Meter, data []byte) (*ParsedEvent, error) {
	// Parse CloudEvents data
	var (
		event interface{}
		err   error
	)

	parsedEvent := &ParsedEvent{
		GroupBy: map[string]string{},
	}

	if len(data) > 0 {
		err = json.Unmarshal(data, &event)
		if err != nil {
			return parsedEvent, NewErrInvalidEvent(fmt.Errorf("failed to parse event data: %w", err))
		}
	}

	// Parse group by fields
	parsedEvent.GroupBy = parseGroupBy(meter, event)

	// We can skip count events as they don't have value property
	if meter.Aggregation == MeterAggregationCount {
		parsedEvent.Value = lo.ToPtr(1.0)

		return parsedEvent, nil
	}

	// Non count events require value property to be present
	// If the event data is null, we return an error as value property is missing
	if event == nil {
		return parsedEvent, NewErrInvalidEvent(errors.New("null and missing value property"))
	}

	if meter.ValueProperty == nil {
		return parsedEvent, NewErrInvalidEvent(errors.New("non count meter value property is missing"))
	}

	// Get value from event data by value property
	var rawValue interface{}

	rawValue, err = jsonpath.JsonPathLookup(event, *meter.ValueProperty)
	if err != nil {
		return parsedEvent, NewErrInvalidEvent(fmt.Errorf("missing value property: %q", *meter.ValueProperty))
	}

	if rawValue == nil {
		return parsedEvent, NewErrInvalidEvent(errors.New("value cannot be null"))
	}

	// Aggregation specific value validation
	switch meter.Aggregation {
	// UNIQUE_COUNT aggregation requires string property value
	case MeterAggregationUniqueCount:
		// We convert the value to string
		parsedEvent.ValueString = lo.ToPtr(fmt.Sprintf("%v", rawValue))

		return parsedEvent, nil
	// SUM, AVG, MIN, MAX, LATEST aggregations require float64 parsable value property value
	case MeterAggregationSum, MeterAggregationAvg, MeterAggregationMin, MeterAggregationMax, MeterAggregationLatest:
		switch v := rawValue.(type) {
		case string:
			parsedValue, err := strconv.ParseFloat(v, 64)
			if err != nil {
				// TODO: omit value or make sure it's length is not too long
				return parsedEvent, NewErrInvalidEvent(fmt.Errorf("value cannot be parsed as float64: %s", v))
			}

			if err := validateFloat64(parsedValue); err != nil {
				return parsedEvent, NewErrInvalidEvent(err)
			}

			parsedEvent.Value = lo.ToPtr(parsedValue)

			return parsedEvent, nil
		case float64:
			if err := validateFloat64(v); err != nil {
				return parsedEvent, NewErrInvalidEvent(err)
			}

			parsedEvent.Value = lo.ToPtr(v)

			return parsedEvent, nil
		default:
			return parsedEvent, NewErrInvalidEvent(fmt.Errorf("unsupported value property type: %T", v))
		}
	}

	return parsedEvent, NewErrInvalidMeter(fmt.Errorf("unknown meter aggregation: %s", meter.Aggregation))
}

// valiodateFloat64 validates a float64 value
func validateFloat64(v float64) error {
	if math.IsNaN(v) {
		return errors.New("value cannot be NaN")
	}

	if math.IsInf(v, 0) {
		return errors.New("value cannot be infinity")
	}

	return nil
}

// parseGroupBy parses the group by fields from the event data
// we allow the group by fields to be missing in the event data or the data to be null
// in such cases we set the group by value to empty string
func parseGroupBy(meter Meter, data interface{}) map[string]string {
	groupBy := map[string]string{}

	// Group by fields
	for groupByKey, groupByPath := range meter.GroupBy {
		var groupByValue string

		rawGroupBy, err := jsonpath.JsonPathLookup(data, groupByPath)
		if err != nil {
			groupByValue = ""
		} else {
			groupByValue = fmt.Sprintf("%v", rawGroupBy)
		}

		groupBy[groupByKey] = groupByValue
	}

	return groupBy
}