internal: propose custom types for image types, arches, etc.
We currently use strings for passing arches and image types around, which is not ideal. We should have a finite set of supported image types and architectures as well as upload targets. This PR introduces custom types to make the code base more readable and possibly also more correct. I considered some alternatives like a struct with private fields, but struct cannot be const, so that does not help either. Eventually I think this is the "get s**t done" solution. The package also includes unit-tests which try to convert string to structure and the other way around to make sure it all works properly.
This commit is contained in:
parent
aab8a4d305
commit
8dac72f4fd
2 changed files with 313 additions and 0 deletions
212
internal/common/types.go
Normal file
212
internal/common/types.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CustomJsonConversionError is thrown when parsing strings into enumerations
|
||||
type CustomJsonConversionError struct {
|
||||
reason string
|
||||
}
|
||||
|
||||
func (err *CustomJsonConversionError) Error() string {
|
||||
return err.reason
|
||||
}
|
||||
|
||||
func unmarshalHelper(data []byte, jsonError, typeConversionError string, mapping map[string]int) (int, error) {
|
||||
var stringInput string
|
||||
err := json.Unmarshal(data, &stringInput)
|
||||
if err != nil {
|
||||
return 0, &CustomJsonConversionError{string(data) + jsonError}
|
||||
}
|
||||
value, exists := mapping[stringInput]
|
||||
if !exists {
|
||||
return 0, &CustomJsonConversionError{stringInput + typeConversionError}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func marshalHelper(input int, mapping map[string]int, errorMessage string) ([]byte, error) {
|
||||
for k, v := range mapping {
|
||||
if v == input {
|
||||
return json.Marshal(k)
|
||||
}
|
||||
}
|
||||
return nil, &CustomJsonConversionError{fmt.Sprintf("%d %s", input, errorMessage)}
|
||||
}
|
||||
|
||||
// Architecture represents one of the supported CPU architectures available for images
|
||||
// produced by osbuild-composer. It is represented as an integer because if it
|
||||
// was a string it would unmarshal from JSON just fine even in case that the architecture
|
||||
// was unknown.
|
||||
type Architecture int
|
||||
|
||||
// A list of supported architectures. As the comment above suggests the type system does
|
||||
// not allow to create a type with a custom set of values, so it is possible to use e.g.
|
||||
// 56 instead of an architecture, but as opposed to a string it should be obvious that
|
||||
// hardcoding a number instead of an architecture is just wrong.
|
||||
//
|
||||
// NOTE: If you want to add more constants here, don't forget to add a mapping below
|
||||
const (
|
||||
X86_64 Architecture = iota
|
||||
Aarch64
|
||||
Armv7hl
|
||||
I686
|
||||
Ppc64le
|
||||
S390x
|
||||
)
|
||||
|
||||
// getArchMapping is a helper function that defines the conversion from JSON string value
|
||||
// to Architecture.
|
||||
func getArchMapping() map[string]int {
|
||||
mapping := map[string]int{
|
||||
"x86_64": int(X86_64),
|
||||
"aarch64": int(Aarch64),
|
||||
"armv7hl": int(Armv7hl),
|
||||
"i686": int(I686),
|
||||
"ppc64le": int(Ppc64le),
|
||||
"s390x": int(S390x),
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a custom unmarshaling function to limit the set of allowed values
|
||||
// in case the input is JSON.
|
||||
func (arch Architecture) UnmarshalJSON(data []byte) error {
|
||||
value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid architecture", getArchMapping())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arch = Architecture(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON is a custom marshaling function for our custom Architecture type
|
||||
func (arch Architecture) MarshalJSON() ([]byte, error) {
|
||||
return marshalHelper(int(arch), getArchMapping(), "is not a valid architecture tag")
|
||||
}
|
||||
|
||||
type ImageType int
|
||||
|
||||
// NOTE: If you want to add more constants here, don't forget to add a mapping below
|
||||
const (
|
||||
Alibaba ImageType = iota
|
||||
Azure
|
||||
Aws
|
||||
GoogleCloud
|
||||
HyperV
|
||||
LiveISO
|
||||
OpenStack
|
||||
Qcow2Generic
|
||||
Vmware
|
||||
)
|
||||
|
||||
// getArchMapping is a helper function that defines the conversion from JSON string value
|
||||
// to ImageType.
|
||||
func getImageTypeMapping() map[string]int {
|
||||
mapping := map[string]int{
|
||||
"Alibaba": int(Alibaba),
|
||||
"Azure": int(Azure),
|
||||
"AWS": int(Aws),
|
||||
"Google Cloud": int(GoogleCloud),
|
||||
"Hyper-V": int(HyperV),
|
||||
"LiveISO": int(LiveISO),
|
||||
"OpenStack": int(OpenStack),
|
||||
"qcow2": int(Qcow2Generic),
|
||||
"VMWare": int(Vmware),
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
|
||||
func (imgType ImageType) UnmarshalJSON(data []byte) error {
|
||||
value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid image type", getImageTypeMapping())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imgType = ImageType(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (imgType ImageType) MarshalJSON() ([]byte, error) {
|
||||
return marshalHelper(int(imgType), getImageTypeMapping(), "is not a valid image type tag")
|
||||
}
|
||||
|
||||
type Distribution int
|
||||
|
||||
// NOTE: If you want to add more constants here, don't forget to add a mapping below
|
||||
const (
|
||||
Fedora30 Distribution = iota
|
||||
Fedora31
|
||||
RHEL82
|
||||
)
|
||||
|
||||
// getArchMapping is a helper function that defines the conversion from JSON string value
|
||||
// to Distribution.
|
||||
func getDistributionMapping() map[string]int {
|
||||
mapping := map[string]int{
|
||||
"fedora-30": int(Fedora30),
|
||||
"fedora-31": int(Fedora31),
|
||||
"rhel-8.2": int(RHEL82),
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
|
||||
func (distro Distribution) UnmarshalJSON(data []byte) error {
|
||||
value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid distribution", getDistributionMapping())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
distro = Distribution(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (distro Distribution) MarshalJSON() ([]byte, error) {
|
||||
return marshalHelper(int(distro), getDistributionMapping(), "is not a valid distribution tag")
|
||||
}
|
||||
|
||||
type UploadTarget int
|
||||
|
||||
// NOTE: If you want to add more constants here, don't forget to add a mapping below
|
||||
const (
|
||||
EC2 UploadTarget = iota
|
||||
AzureStorage // I mention "storage" explicitly because we might want to support gallery as well
|
||||
)
|
||||
|
||||
// getArchMapping is a helper function that defines the conversion from JSON string value
|
||||
// to UploadTarget.
|
||||
func getUploadTargetMapping() map[string]int {
|
||||
mapping := map[string]int{
|
||||
"AWS EC2": int(EC2),
|
||||
"Azure storage": int(AzureStorage),
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
|
||||
func (ut UploadTarget) UnmarshalJSON(data []byte) error {
|
||||
value, err := unmarshalHelper(data, " is not a valid JSON value", " is not a valid upload target", getUploadTargetMapping())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ut = UploadTarget(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ut UploadTarget) MarshalJSON() ([]byte, error) {
|
||||
return marshalHelper(int(ut), getUploadTargetMapping(), "is not a valid upload target tag")
|
||||
}
|
||||
|
||||
type ImageRequest struct {
|
||||
ImgType ImageType `json:"image_type"`
|
||||
UpTarget []UploadTarget `json:"upload_targets"`
|
||||
}
|
||||
|
||||
// ComposeRequest is used to submit a new compose to the store
|
||||
type ComposeRequest struct {
|
||||
BlueprintName string `json:"blueprint_name"`
|
||||
ComposeID uuid.UUID `json:"uuid"`
|
||||
Distro Distribution `json:"distro"`
|
||||
Arch Architecture `json:"arch"`
|
||||
RequestedImages []ImageRequest `json:"requested_images"`
|
||||
}
|
||||
101
internal/common/types_test.go
Normal file
101
internal/common/types_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/google/uuid"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSONConversionsComposeRequest(t *testing.T) {
|
||||
cases := []struct{
|
||||
input string
|
||||
expectedConversionResult ComposeRequest
|
||||
}{
|
||||
// 1st case
|
||||
{
|
||||
`
|
||||
{
|
||||
"blueprint_name": "foo",
|
||||
"uuid": "789b4d42-da1a-49c9-a20c-054da3bb6c82",
|
||||
"distro": "fedora-31",
|
||||
"arch": "x86_64",
|
||||
"requested_images": [
|
||||
{
|
||||
"image_type": "AWS",
|
||||
"upload_targets": ["AWS EC2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
ComposeRequest{
|
||||
BlueprintName: "foo",
|
||||
ComposeID: uuid.UUID{},
|
||||
Distro: Fedora31,
|
||||
Arch: X86_64,
|
||||
RequestedImages: []ImageRequest{{
|
||||
ImgType: Aws,
|
||||
UpTarget: []UploadTarget{EC2},
|
||||
}},
|
||||
},
|
||||
},
|
||||
// 2nd case
|
||||
{
|
||||
`
|
||||
{
|
||||
"blueprint_name": "bar",
|
||||
"uuid": "789b4d42-da1a-49c9-a20c-054da3bb6c82",
|
||||
"distro": "rhel-8.2",
|
||||
"arch": "aarch64",
|
||||
"requested_images": [
|
||||
{
|
||||
"image_type": "Azure",
|
||||
"upload_targets": ["Azure storage", "AWS EC2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
ComposeRequest{
|
||||
BlueprintName: "bar",
|
||||
ComposeID: uuid.UUID{},
|
||||
Distro: RHEL82,
|
||||
Arch: Aarch64,
|
||||
RequestedImages: []ImageRequest{{
|
||||
ImgType: Azure,
|
||||
UpTarget: []UploadTarget{AzureStorage, EC2},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
// Test unmashaling the JSON from the string above
|
||||
var inputStringAsStruct *ComposeRequest
|
||||
err := json.Unmarshal([]byte(c.input), &inputStringAsStruct)
|
||||
if err != nil {
|
||||
t.Fatal("Failed ot unmarshal ComposeRequest:", err)
|
||||
}
|
||||
if reflect.DeepEqual(inputStringAsStruct, c.expectedConversionResult) {
|
||||
t.Error("Unmarshaled compose request is not the one expected")
|
||||
}
|
||||
|
||||
// Test marshaling the expected structure into JSON byte array, but since JSON package in golang std lib
|
||||
// does not have a canonical form (a 3rd party library is necessary) I convert it back to struct and
|
||||
// compare the resulting structure with the input one
|
||||
data, err := json.Marshal(c.expectedConversionResult)
|
||||
if err != nil {
|
||||
t.Fatal("Failed ot marshal ComposeRequest:", err)
|
||||
}
|
||||
var expectedResultAfterMarshaling *ComposeRequest
|
||||
err = json.Unmarshal(data, &expectedResultAfterMarshaling)
|
||||
if err != nil {
|
||||
t.Fatal("Failed ot unmarshal ComposeRequest:", err, ", input:", string(data))
|
||||
}
|
||||
if reflect.DeepEqual(expectedResultAfterMarshaling, c.expectedConversionResult) {
|
||||
t.Error("Marshaled compose request is not the one expected")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue