Skip to content

Commit 6121538

Browse files
committed
provide serialization support
Signed-off-by: grokspawn <jordan@nimblewidget.com>
1 parent 17f2fa5 commit 6121538

2 files changed

Lines changed: 308 additions & 6 deletions

File tree

alpha/declcfg/declcfg.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,21 @@ func NewRelease(relStr string) (Release, error) {
229229
return nil, nil
230230
}
231231

232+
// Validate against CRD constraint from operators.coreos.com/v1alpha1 ClusterServiceVersion
233+
// Maximum length of 20 characters
234+
if len(relStr) > 20 {
235+
return nil, fmt.Errorf("invalid release %q: exceeds maximum length of 20 characters", relStr)
236+
}
237+
232238
var (
233239
segments = strings.Split(relStr, ".")
234240
r = make(Release, 0, len(segments))
235241
errs []error
236242
)
237243
for i, segment := range segments {
244+
// semver.NewPRVersion validates:
245+
// - Pattern: alphanumerics and hyphens only
246+
// - No leading zeros in numeric identifiers
238247
prVer, err := semver.NewPRVersion(segment)
239248
if err != nil {
240249
errs = append(errs, fmt.Errorf("segment %d: %v", i, err))
@@ -249,15 +258,15 @@ func NewRelease(relStr string) (Release, error) {
249258
}
250259

251260
type CompositeVersion struct {
252-
version semver.Version
253-
release Release
261+
Version semver.Version `json:"version"`
262+
Release Release `json:"release,omitempty"`
254263
}
255264

256265
func (cv *CompositeVersion) Compare(other *CompositeVersion) int {
257-
if cmp := cv.version.Compare(other.version); cmp != 0 {
266+
if cmp := cv.Version.Compare(other.Version); cmp != 0 {
258267
return cmp
259268
}
260-
return cv.release.Compare(other.release)
269+
return cv.Release.Compare(other.Release)
261270
}
262271

263272
// order by version, then
@@ -299,7 +308,7 @@ func (b *Bundle) CompositeVersion() (*CompositeVersion, error) {
299308
}
300309

301310
return &CompositeVersion{
302-
version: v,
303-
release: r,
311+
Version: v,
312+
Release: r,
304313
}, nil
305314
}

alpha/declcfg/declcfg_test.go

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package declcfg
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/blang/semver/v4"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestCompositeVersion_MarshalJSON(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
cv *CompositeVersion
16+
expected string
17+
}{
18+
{
19+
name: "version only",
20+
cv: &CompositeVersion{
21+
Version: semver.MustParse("1.2.3"),
22+
Release: nil,
23+
},
24+
expected: `{"version":"1.2.3"}`,
25+
},
26+
{
27+
name: "version with release",
28+
cv: &CompositeVersion{
29+
Version: semver.MustParse("1.2.3"),
30+
Release: Release{
31+
semver.PRVersion{VersionStr: "alpha"},
32+
semver.PRVersion{VersionNum: 1, IsNum: true},
33+
},
34+
},
35+
expected: `{"version":"1.2.3","release":[{"VersionStr":"alpha","VersionNum":0,"IsNum":false},{"VersionStr":"","VersionNum":1,"IsNum":true}]}`,
36+
},
37+
{
38+
name: "version with empty release",
39+
cv: &CompositeVersion{
40+
Version: semver.MustParse("0.1.0"),
41+
Release: Release{},
42+
},
43+
expected: `{"version":"0.1.0"}`,
44+
},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
data, err := json.Marshal(tt.cv)
50+
require.NoError(t, err)
51+
assert.JSONEq(t, tt.expected, string(data))
52+
})
53+
}
54+
}
55+
56+
func TestCompositeVersion_UnmarshalJSON(t *testing.T) {
57+
tests := []struct {
58+
name string
59+
input string
60+
expected *CompositeVersion
61+
wantErr bool
62+
}{
63+
{
64+
name: "version only",
65+
input: `{"version":"1.2.3"}`,
66+
expected: &CompositeVersion{
67+
Version: semver.MustParse("1.2.3"),
68+
Release: nil,
69+
},
70+
},
71+
{
72+
name: "version with release",
73+
input: `{"version":"1.2.3","release":[{"VersionStr":"alpha","VersionNum":0,"IsNum":false},{"VersionStr":"","VersionNum":1,"IsNum":true}]}`,
74+
expected: &CompositeVersion{
75+
Version: semver.MustParse("1.2.3"),
76+
Release: Release{
77+
semver.PRVersion{VersionStr: "alpha"},
78+
semver.PRVersion{VersionNum: 1, IsNum: true},
79+
},
80+
},
81+
},
82+
{
83+
name: "version with empty release",
84+
input: `{"version":"0.1.0","release":[]}`,
85+
expected: &CompositeVersion{
86+
Version: semver.MustParse("0.1.0"),
87+
Release: Release{},
88+
},
89+
},
90+
{
91+
name: "invalid json",
92+
input: `{invalid}`,
93+
wantErr: true,
94+
},
95+
{
96+
name: "invalid version",
97+
input: `{"version":"not-a-version"}`,
98+
wantErr: true,
99+
},
100+
}
101+
102+
for _, tt := range tests {
103+
t.Run(tt.name, func(t *testing.T) {
104+
var cv CompositeVersion
105+
err := json.Unmarshal([]byte(tt.input), &cv)
106+
if tt.wantErr {
107+
require.Error(t, err)
108+
return
109+
}
110+
require.NoError(t, err)
111+
assert.Equal(t, tt.expected.Version, cv.Version)
112+
assert.Equal(t, tt.expected.Release, cv.Release)
113+
})
114+
}
115+
}
116+
117+
func TestCompositeVersion_MarshalUnmarshalRoundTrip(t *testing.T) {
118+
tests := []struct {
119+
name string
120+
cv *CompositeVersion
121+
}{
122+
{
123+
name: "version only",
124+
cv: &CompositeVersion{
125+
Version: semver.MustParse("2.5.1"),
126+
Release: nil,
127+
},
128+
},
129+
{
130+
name: "version with release",
131+
cv: &CompositeVersion{
132+
Version: semver.MustParse("1.0.0"),
133+
Release: Release{
134+
semver.PRVersion{VersionStr: "beta"},
135+
semver.PRVersion{VersionNum: 2, IsNum: true},
136+
},
137+
},
138+
},
139+
{
140+
name: "complex version with metadata",
141+
cv: &CompositeVersion{
142+
Version: semver.Version{
143+
Major: 3,
144+
Minor: 4,
145+
Patch: 5,
146+
Pre: []semver.PRVersion{
147+
{VersionStr: "rc"},
148+
{VersionNum: 1, IsNum: true},
149+
},
150+
Build: []string{"build", "123"},
151+
},
152+
Release: Release{
153+
semver.PRVersion{VersionStr: "rel"},
154+
semver.PRVersion{VersionNum: 42, IsNum: true},
155+
},
156+
},
157+
},
158+
}
159+
160+
for _, tt := range tests {
161+
t.Run(tt.name, func(t *testing.T) {
162+
// Marshal
163+
data, err := json.Marshal(tt.cv)
164+
require.NoError(t, err)
165+
166+
// Unmarshal
167+
var result CompositeVersion
168+
err = json.Unmarshal(data, &result)
169+
require.NoError(t, err)
170+
171+
// Compare
172+
assert.Equal(t, tt.cv.Version, result.Version)
173+
assert.Equal(t, tt.cv.Release, result.Release)
174+
assert.Equal(t, 0, tt.cv.Compare(&result), "round-tripped CompositeVersion should compare equal")
175+
})
176+
}
177+
}
178+
179+
func TestNewRelease(t *testing.T) {
180+
tests := []struct {
181+
name string
182+
input string
183+
expected Release
184+
wantErr bool
185+
errMsg string
186+
}{
187+
{
188+
name: "empty string",
189+
input: "",
190+
expected: nil,
191+
wantErr: false,
192+
},
193+
{
194+
name: "single alphanumeric segment",
195+
input: "alpha",
196+
expected: Release{
197+
semver.PRVersion{VersionStr: "alpha"},
198+
},
199+
wantErr: false,
200+
},
201+
{
202+
name: "single numeric segment",
203+
input: "1",
204+
expected: Release{
205+
semver.PRVersion{VersionNum: 1, IsNum: true},
206+
},
207+
wantErr: false,
208+
},
209+
{
210+
name: "multiple segments",
211+
input: "alpha.1.beta.2",
212+
expected: Release{
213+
semver.PRVersion{VersionStr: "alpha"},
214+
semver.PRVersion{VersionNum: 1, IsNum: true},
215+
semver.PRVersion{VersionStr: "beta"},
216+
semver.PRVersion{VersionNum: 2, IsNum: true},
217+
},
218+
wantErr: false,
219+
},
220+
{
221+
name: "hyphens allowed",
222+
input: "rc-1.beta-2",
223+
expected: Release{
224+
semver.PRVersion{VersionStr: "rc-1"},
225+
semver.PRVersion{VersionStr: "beta-2"},
226+
},
227+
wantErr: false,
228+
},
229+
{
230+
name: "max length 20 characters",
231+
input: "12345678901234567890",
232+
expected: Release{
233+
semver.PRVersion{VersionNum: 12345678901234567890, IsNum: true},
234+
},
235+
wantErr: false,
236+
},
237+
{
238+
name: "exceeds max length",
239+
input: "123456789012345678901",
240+
wantErr: true,
241+
errMsg: "exceeds maximum length of 20 characters",
242+
},
243+
{
244+
name: "leading zeros in numeric segment",
245+
input: "01",
246+
wantErr: true,
247+
errMsg: "Numeric PreRelease version must not contain leading zeroes",
248+
},
249+
{
250+
name: "leading zeros in multiple digit numeric segment",
251+
input: "001",
252+
wantErr: true,
253+
errMsg: "Numeric PreRelease version must not contain leading zeroes",
254+
},
255+
{
256+
name: "zero without leading zeros is valid",
257+
input: "0",
258+
expected: Release{
259+
semver.PRVersion{VersionNum: 0, IsNum: true},
260+
},
261+
wantErr: false,
262+
},
263+
{
264+
name: "alphanumeric starting with zero is valid",
265+
input: "0alpha",
266+
expected: Release{
267+
semver.PRVersion{VersionStr: "0alpha"},
268+
},
269+
wantErr: false,
270+
},
271+
{
272+
name: "invalid characters",
273+
input: "alpha_beta",
274+
wantErr: true,
275+
errMsg: "Invalid character",
276+
},
277+
}
278+
279+
for _, tt := range tests {
280+
t.Run(tt.name, func(t *testing.T) {
281+
result, err := NewRelease(tt.input)
282+
if tt.wantErr {
283+
require.Error(t, err)
284+
if tt.errMsg != "" {
285+
assert.Contains(t, err.Error(), tt.errMsg)
286+
}
287+
return
288+
}
289+
require.NoError(t, err)
290+
assert.Equal(t, tt.expected, result)
291+
})
292+
}
293+
}

0 commit comments

Comments
 (0)