forked from osbuild/images
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
datasizes: add new
Size
type with json/toml decoding support
This commit adds a new `datasizes.Size` type that is an alias for uint64 but has supports direct json/toml decoding. So sizes can be specified as 123, "123" or "123 GiB" and this is transparently handled. datasizes: tweak error return to avoid "stuttering" The `datasizes.Size.UnmarshalJSON()` returns a `JSON unmarshal` prefix. However this is a bit too generic as other (nested) unmarshalers may also use the prefix. So instead just indicate the type in the error string itself: ``` error decoding {TOML,JSON} size: ... ```
- Loading branch information
Showing
2 changed files
with
197 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package datasizes | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
) | ||
|
||
// Size is a wrapper around uint64 with support for reading from string | ||
// yaml/toml, so {"size": 123}, {"size": "1234"}, {"size": "1 GiB"} are | ||
// all supported | ||
type Size uint64 | ||
|
||
// Uint64 returns the size as uint64. This is a convenience functions, | ||
// it is strictly equivalent to uint64(Size(1)) | ||
func (si Size) Uint64() uint64 { | ||
return uint64(si) | ||
} | ||
|
||
func (si *Size) UnmarshalTOML(data interface{}) error { | ||
i, err := decodeSize(data) | ||
if err != nil { | ||
return fmt.Errorf("error decoding TOML size: %w", err) | ||
} | ||
*si = Size(i) | ||
return nil | ||
} | ||
|
||
func (si *Size) UnmarshalJSON(data []byte) error { | ||
dec := json.NewDecoder(bytes.NewBuffer(data)) | ||
dec.UseNumber() | ||
|
||
var v interface{} | ||
if err := dec.Decode(&v); err != nil { | ||
return err | ||
} | ||
i, err := decodeSize(v) | ||
if err != nil { | ||
// if only we could do better here and include e.g. the field | ||
// name where this happend but encoding/json does not | ||
// support this, c.f. https://github.com/golang/go/issues/58655 | ||
return fmt.Errorf("error decoding size: %w", err) | ||
} | ||
*si = Size(i) | ||
return nil | ||
} | ||
|
||
// decodeSize takes an integer or string representing a data size (with a data | ||
// suffix) and returns the uint64 representation. | ||
func decodeSize(size any) (uint64, error) { | ||
switch s := size.(type) { | ||
case string: | ||
return Parse(s) | ||
case json.Number: | ||
i, err := s.Int64() | ||
if i < 0 { | ||
return 0, fmt.Errorf("cannot be negative") | ||
} | ||
return uint64(i), err | ||
case int64: | ||
if s < 0 { | ||
return 0, fmt.Errorf("cannot be negative") | ||
} | ||
return uint64(s), nil | ||
case uint64: | ||
return s, nil | ||
case float64, float32: | ||
return 0, fmt.Errorf("cannot be float") | ||
default: | ||
return 0, fmt.Errorf("failed to convert value \"%v\" to number", size) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package datasizes_test | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
|
||
"github.com/BurntSushi/toml" | ||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/osbuild/images/pkg/datasizes" | ||
) | ||
|
||
func TestSizeUnmarshalTOMLUnhappy(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
input string | ||
err string | ||
}{ | ||
{ | ||
name: "wrong datatype/bool", | ||
input: `size = true`, | ||
err: `toml: line 1 (last key "size"): error decoding TOML size: failed to convert value "true" to number`, | ||
}, | ||
{ | ||
name: "wrong datatype/float", | ||
input: `size = 3.14`, | ||
err: `toml: line 1 (last key "size"): error decoding TOML size: cannot be float`, | ||
}, | ||
{ | ||
name: "wrong unit", | ||
input: `size = "20 KG"`, | ||
err: `toml: line 1 (last key "size"): error decoding TOML size: unknown data size units in string: 20 KG`, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
var v struct { | ||
Size datasizes.Size `toml:"size"` | ||
} | ||
err := toml.Unmarshal([]byte(tc.input), &v) | ||
assert.EqualError(t, err, tc.err, tc.input) | ||
}) | ||
} | ||
} | ||
|
||
func TestSizeUnmarshalJSONUnhappy(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
input string | ||
err string | ||
}{ | ||
{ | ||
name: "misize nor string nor int", | ||
input: `{"size": true}`, | ||
err: `error decoding size: failed to convert value "true" to number`, | ||
}, | ||
{ | ||
name: "wrong datatype/float", | ||
input: `{"size": 3.14}`, | ||
err: `error decoding size: strconv.ParseInt: parsing "3.14": invalid syntax`, | ||
}, | ||
{ | ||
name: "misize not parseable", | ||
input: `{"size": "20 KG"}`, | ||
err: `error decoding size: unknown data size units in string: 20 KG`, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
var v struct { | ||
Size datasizes.Size `json:"size"` | ||
} | ||
err := json.Unmarshal([]byte(tc.input), &v) | ||
assert.EqualError(t, err, tc.err, tc.input) | ||
}) | ||
} | ||
} | ||
|
||
func TestSizeUnmarshalHappy(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
inputJSON string | ||
inputTOML string | ||
expected datasizes.Size | ||
}{ | ||
{ | ||
name: "int", | ||
inputJSON: `{"size": 1234}`, | ||
inputTOML: `size = 1234`, | ||
expected: 1234, | ||
}, | ||
{ | ||
name: "str", | ||
inputJSON: `{"size": "1234"}`, | ||
inputTOML: `size = "1234"`, | ||
expected: 1234, | ||
}, | ||
{ | ||
name: "str/with-unit", | ||
inputJSON: `{"size": "1234 MiB"}`, | ||
inputTOML: `size = "1234 MiB"`, | ||
expected: 1234 * datasizes.MiB, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
var v struct { | ||
Size datasizes.Size `json:"size" toml:"size"` | ||
} | ||
err := toml.Unmarshal([]byte(tc.inputTOML), &v) | ||
assert.NoError(t, err) | ||
assert.Equal(t, tc.expected, v.Size, tc.inputTOML) | ||
err = json.Unmarshal([]byte(tc.inputJSON), &v) | ||
assert.NoError(t, err) | ||
assert.Equal(t, tc.expected, v.Size, tc.inputJSON) | ||
}) | ||
} | ||
} | ||
|
||
func TestSizeUint64(t *testing.T) { | ||
assert.Equal(t, datasizes.Size(1234).Uint64(), uint64(1234)) | ||
} |