-
Notifications
You must be signed in to change notification settings - Fork 50
/
semver.go
150 lines (131 loc) · 4.25 KB
/
semver.go
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
package evaluator
import (
"errors"
"fmt"
"strings"
"github.com/open-feature/flagd/core/pkg/logger"
"golang.org/x/mod/semver"
)
const SemVerEvaluationName = "sem_ver"
type SemVerOperator string
const (
Equals SemVerOperator = "="
NotEqual SemVerOperator = "!="
Less SemVerOperator = "<"
LessOrEqual SemVerOperator = "<="
GreaterOrEqual SemVerOperator = ">="
Greater SemVerOperator = ">"
MatchMajor SemVerOperator = "^"
MatchMinor SemVerOperator = "~"
)
func (svo SemVerOperator) compare(v1, v2 string) (bool, error) {
cmpRes := semver.Compare(v1, v2)
switch svo {
case Less:
return cmpRes == -1, nil
case Equals:
return cmpRes == 0, nil
case NotEqual:
return cmpRes != 0, nil
case LessOrEqual:
return cmpRes == -1 || cmpRes == 0, nil
case GreaterOrEqual:
return cmpRes == +1 || cmpRes == 0, nil
case Greater:
return cmpRes == +1, nil
case MatchMinor:
v1MajorMinor := semver.MajorMinor(v1)
v2MajorMinor := semver.MajorMinor(v2)
return semver.Compare(v1MajorMinor, v2MajorMinor) == 0, nil
case MatchMajor:
v1Major := semver.Major(v1)
v2Major := semver.Major(v2)
return semver.Compare(v1Major, v2Major) == 0, nil
default:
return false, errors.New("invalid operator")
}
}
type SemVerComparison struct {
Logger *logger.Logger
}
func NewSemVerComparison(log *logger.Logger) *SemVerComparison {
return &SemVerComparison{Logger: log}
}
// SemVerEvaluation checks if the given property matches a semantic versioning condition.
// It returns 'true', if the value of the given property meets the condition, 'false' if not.
// As an example, it can be used in the following way inside an 'if' evaluation:
//
// {
// "if": [
// {
// "sem_ver": [{"var": "version"}, ">=", "1.0.0"]
// },
// "red", null
// ]
// }
//
// This rule can be applied to the following data object, where the evaluation will resolve to 'true':
//
// { "version": "2.0.0" }
//
// Note that the 'sem_ver' evaluation rule must contain exactly three items:
// 1. Target property: this needs which both resolve to a semantic versioning string
// 2. Operator: One of the following: '=', '!=', '>', '<', '>=', '<=', '~', '^'
// 3. Target value: this needs which both resolve to a semantic versioning string
func (je *SemVerComparison) SemVerEvaluation(values, _ interface{}) interface{} {
actualVersion, targetVersion, operator, err := parseSemverEvaluationData(values)
if err != nil {
je.Logger.Error(fmt.Sprintf("parse sem_ver evaluation data: %v", err))
return false
}
res, err := operator.compare(actualVersion, targetVersion)
if err != nil {
je.Logger.Error(fmt.Sprintf("sem_ver evaluation: %v", err))
return false
}
return res
}
func parseSemverEvaluationData(values interface{}) (string, string, SemVerOperator, error) {
parsed, ok := values.([]interface{})
if !ok {
return "", "", "", errors.New("sem_ver evaluation is not an array")
}
if len(parsed) != 3 {
return "", "", "", errors.New("sem_ver evaluation must contain a value, an operator and a comparison target")
}
actualVersion, err := parseSemanticVersion(parsed[0])
if err != nil {
return "", "", "", fmt.Errorf("sem_ver evaluation: could not parse target property value: %w", err)
}
operator, err := parseOperator(parsed[1])
if err != nil {
return "", "", "", fmt.Errorf("sem_ver evaluation: could not parse operator: %w", err)
}
targetVersion, err := parseSemanticVersion(parsed[2])
if err != nil {
return "", "", "", fmt.Errorf("sem_ver evaluation: could not parse target value: %w", err)
}
return actualVersion, targetVersion, operator, nil
}
func parseSemanticVersion(v interface{}) (string, error) {
version, ok := v.(string)
if !ok {
return "", errors.New("sem_ver evaluation: property did not resolve to a string value")
}
// version strings are only valid in the semver package if they start with a 'v'
// if it's not present in the given value, we prepend it
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
if !semver.IsValid(version) {
return "", errors.New("not a valid semantic version string")
}
return version, nil
}
func parseOperator(o interface{}) (SemVerOperator, error) {
operatorString, ok := o.(string)
if !ok {
return "", errors.New("could not parse operator")
}
return SemVerOperator(operatorString), nil
}