feat(schema): add JSON marshaling for ModuleSchema (#21371)
This commit is contained in:
parent
8ca94d9acb
commit
89ec787f7e
@ -12,16 +12,16 @@ type EnumType struct {
|
||||
// Its name must be unique between all enum types and object types in the module.
|
||||
// The same enum, however, can be used in multiple object types and fields as long as the
|
||||
// definition is identical each time.
|
||||
Name string
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Values is a list of distinct, non-empty values that are part of the enum type.
|
||||
// Each value must conform to the NameFormat regular expression.
|
||||
Values []EnumValueDefinition
|
||||
Values []EnumValueDefinition `json:"values"`
|
||||
|
||||
// NumericKind is the numeric kind used to represent the enum values numerically.
|
||||
// If it is left empty, Int32Kind is used by default.
|
||||
// Valid values are Uint8Kind, Int8Kind, Uint16Kind, Int16Kind, and Int32Kind.
|
||||
NumericKind Kind
|
||||
NumericKind Kind `json:"numeric_kind,omitempty"`
|
||||
}
|
||||
|
||||
// EnumValueDefinition represents a value in an enum type.
|
||||
@ -29,11 +29,11 @@ type EnumValueDefinition struct {
|
||||
// Name is the name of the enum value.
|
||||
// It must conform to the NameFormat regular expression.
|
||||
// Its name must be unique between all values in the enum.
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
|
||||
// Value is the numeric value of the enum.
|
||||
// It must be unique between all values in the enum.
|
||||
Value int32
|
||||
Value int32 `json:"value"`
|
||||
}
|
||||
|
||||
// TypeName implements the Type interface.
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Field represents a field in an object type.
|
||||
type Field struct {
|
||||
// Name is the name of the field. It must conform to the NameFormat regular expression.
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
|
||||
// Kind is the basic type of the field.
|
||||
Kind Kind
|
||||
Kind Kind `json:"kind"`
|
||||
|
||||
// Nullable indicates whether null values are accepted for the field. Key fields CANNOT be nullable.
|
||||
Nullable bool
|
||||
Nullable bool `json:"nullable,omitempty"`
|
||||
|
||||
// ReferencedType is the referenced type name when Kind is EnumKind.
|
||||
ReferencedType string
|
||||
ReferencedType string `json:"referenced_type,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates the field.
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -165,6 +167,64 @@ func TestField_ValidateValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldJSON(t *testing.T) {
|
||||
tt := []struct {
|
||||
field Field
|
||||
json string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
field: Field{
|
||||
Name: "field1",
|
||||
Kind: StringKind,
|
||||
},
|
||||
json: `{"name":"field1","kind":"string"}`,
|
||||
},
|
||||
{
|
||||
field: Field{
|
||||
Name: "field1",
|
||||
Kind: Int32Kind,
|
||||
Nullable: true,
|
||||
},
|
||||
json: `{"name":"field1","kind":"int32","nullable":true}`,
|
||||
},
|
||||
{
|
||||
field: Field{
|
||||
Name: "field1",
|
||||
Kind: EnumKind,
|
||||
ReferencedType: "enum",
|
||||
},
|
||||
json: `{"name":"field1","kind":"enum","referenced_type":"enum"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.json, func(t *testing.T) {
|
||||
b, err := json.Marshal(tc.field)
|
||||
if tc.expectErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(b) != tc.json {
|
||||
t.Fatalf("expected %s, got %s", tc.json, string(b))
|
||||
}
|
||||
var field Field
|
||||
err = json.Unmarshal(b, &field)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(field, tc.field) {
|
||||
t.Fatalf("expected %v, got %v", tc.field, field)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var testEnumSchema = MustNewModuleSchema(EnumType{
|
||||
Name: "enum",
|
||||
Values: []EnumValueDefinition{{Name: "a", Value: 1}, {Name: "b", Value: 2}},
|
||||
|
||||
@ -431,3 +431,34 @@ func KindForGoValue(value interface{}) Kind {
|
||||
return InvalidKind
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the kind to a JSON string and returns an error if the kind is invalid.
|
||||
func (t Kind) MarshalJSON() ([]byte, error) {
|
||||
if err := t.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the kind from a JSON string and returns an error if the kind is invalid.
|
||||
func (t *Kind) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
err := json.Unmarshal(data, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k, ok := kindStrings[s]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid kind: %s", s)
|
||||
}
|
||||
*t = k
|
||||
return nil
|
||||
}
|
||||
|
||||
var kindStrings = map[string]Kind{}
|
||||
|
||||
func init() {
|
||||
for i := InvalidKind + 1; i <= MAX_VALID_KIND; i++ {
|
||||
kindStrings[i.String()] = i
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,3 +263,58 @@ func TestKindForGoValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKindJSON(t *testing.T) {
|
||||
tt := []struct {
|
||||
kind Kind
|
||||
want string
|
||||
expectErr bool
|
||||
}{
|
||||
{StringKind, `"string"`, false},
|
||||
{BytesKind, `"bytes"`, false},
|
||||
{Int8Kind, `"int8"`, false},
|
||||
{Uint8Kind, `"uint8"`, false},
|
||||
{Int16Kind, `"int16"`, false},
|
||||
{Uint16Kind, `"uint16"`, false},
|
||||
{Int32Kind, `"int32"`, false},
|
||||
{Uint32Kind, `"uint32"`, false},
|
||||
{Int64Kind, `"int64"`, false},
|
||||
{Uint64Kind, `"uint64"`, false},
|
||||
{IntegerStringKind, `"integer"`, false},
|
||||
{DecimalStringKind, `"decimal"`, false},
|
||||
{BoolKind, `"bool"`, false},
|
||||
{TimeKind, `"time"`, false},
|
||||
{DurationKind, `"duration"`, false},
|
||||
{Float32Kind, `"float32"`, false},
|
||||
{Float64Kind, `"float64"`, false},
|
||||
{JSONKind, `"json"`, false},
|
||||
{EnumKind, `"enum"`, false},
|
||||
{AddressKind, `"address"`, false},
|
||||
{InvalidKind, `""`, true},
|
||||
{Kind(100), `""`, true},
|
||||
}
|
||||
for i, tc := range tt {
|
||||
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
||||
b, err := json.Marshal(tc.kind)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error, got nil", i)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: unexpected error: %v", i, err)
|
||||
}
|
||||
if !tc.expectErr {
|
||||
if string(b) != tc.want {
|
||||
t.Errorf("test %d: expected %s, got %s", i, tc.want, string(b))
|
||||
}
|
||||
var k Kind
|
||||
err := json.Unmarshal(b, &k)
|
||||
if err != nil {
|
||||
t.Errorf("test %d: unexpected error: %v", i, err)
|
||||
}
|
||||
if k != tc.kind {
|
||||
t.Errorf("test %d: expected %s, got %s", i, tc.kind, k)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
@ -113,4 +114,59 @@ func (s ModuleSchema) EnumTypes(f func(EnumType) bool) {
|
||||
})
|
||||
}
|
||||
|
||||
type moduleSchemaJson struct {
|
||||
ObjectTypes []ObjectType `json:"object_types"`
|
||||
EnumTypes []EnumType `json:"enum_types"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for ModuleSchema.
|
||||
// It marshals the module schema into a JSON object with the object types and enum types
|
||||
// under the keys "object_types" and "enum_types" respectively.
|
||||
func (s ModuleSchema) MarshalJSON() ([]byte, error) {
|
||||
asJson := moduleSchemaJson{}
|
||||
|
||||
s.ObjectTypes(func(objType ObjectType) bool {
|
||||
asJson.ObjectTypes = append(asJson.ObjectTypes, objType)
|
||||
return true
|
||||
})
|
||||
|
||||
s.EnumTypes(func(enumType EnumType) bool {
|
||||
asJson.EnumTypes = append(asJson.EnumTypes, enumType)
|
||||
return true
|
||||
})
|
||||
|
||||
return json.Marshal(asJson)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for ModuleSchema.
|
||||
// See MarshalJSON for the JSON format.
|
||||
func (s *ModuleSchema) UnmarshalJSON(data []byte) error {
|
||||
asJson := moduleSchemaJson{}
|
||||
|
||||
err := json.Unmarshal(data, &asJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
types := map[string]Type{}
|
||||
|
||||
for _, objType := range asJson.ObjectTypes {
|
||||
types[objType.Name] = objType
|
||||
}
|
||||
|
||||
for _, enumType := range asJson.EnumTypes {
|
||||
types[enumType.Name] = enumType
|
||||
}
|
||||
|
||||
s.types = types
|
||||
|
||||
// validate adds all enum types to the type map
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ Schema = ModuleSchema{}
|
||||
|
||||
@ -316,3 +316,27 @@ func TestModuleSchema_EnumTypes(t *testing.T) {
|
||||
t.Fatalf("expected %v, got %v", expected, typeNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSchemaJSON(t *testing.T) {
|
||||
moduleSchema := exampleSchema(t)
|
||||
|
||||
b, err := moduleSchema.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
const expectedJson = `{"object_types":[{"name":"object1","key_fields":[{"name":"field1","kind":"enum","referenced_type":"enum2"}]},{"name":"object2","key_fields":[{"name":"field1","kind":"enum","referenced_type":"enum1"}]}],"enum_types":[{"name":"enum1","values":[{"name":"a","value":1},{"name":"b","value":2},{"name":"c","value":3}]},{"name":"enum2","values":[{"name":"d","value":4},{"name":"e","value":5},{"name":"f","value":6}]}]}`
|
||||
if string(b) != expectedJson {
|
||||
t.Fatalf("expected %s\n, got %s", expectedJson, string(b))
|
||||
}
|
||||
|
||||
var moduleSchema2 ModuleSchema
|
||||
err = moduleSchema2.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(moduleSchema, moduleSchema2) {
|
||||
t.Fatalf("expected %v, got %v", moduleSchema, moduleSchema2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import "fmt"
|
||||
type ObjectType struct {
|
||||
// Name is the name of the object type. It must be unique within the module schema amongst all object and enum
|
||||
// types and conform to the NameFormat regular expression.
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
|
||||
// KeyFields is a list of fields that make up the primary key of the object.
|
||||
// It can be empty in which case indexers should assume that this object is
|
||||
@ -14,19 +14,19 @@ type ObjectType struct {
|
||||
// object between both key and value fields.
|
||||
// Key fields CANNOT be nullable and Float32Kind, Float64Kind, and JSONKind types
|
||||
// are not allowed.
|
||||
KeyFields []Field
|
||||
KeyFields []Field `json:"key_fields,omitempty"`
|
||||
|
||||
// ValueFields is a list of fields that are not part of the primary key of the object.
|
||||
// It can be empty in the case where all fields are part of the primary key.
|
||||
// Field names must be unique within the object between both key and value fields.
|
||||
ValueFields []Field
|
||||
ValueFields []Field `json:"value_fields,omitempty"`
|
||||
|
||||
// RetainDeletions is a flag that indicates whether the indexer should retain
|
||||
// deleted rows in the database and flag them as deleted rather than actually
|
||||
// deleting the row. For many types of data in state, the data is deleted even
|
||||
// though it is still valid in order to save space. Indexers will want to have
|
||||
// the option of retaining such data and distinguishing from other "true" deletions.
|
||||
RetainDeletions bool
|
||||
RetainDeletions bool `json:"retain_deletions,omitempty"`
|
||||
}
|
||||
|
||||
// TypeName implements the Type interface.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
InitializeModuleData: {"ModuleName":"all_kinds","Schema":{}}
|
||||
InitializeModuleData: {"ModuleName":"test_cases","Schema":{}}
|
||||
InitializeModuleData: {"ModuleName":"all_kinds","Schema":{"object_types":[{"name":"test_address","key_fields":[{"name":"key","kind":"address"}],"value_fields":[{"name":"valNotNull","kind":"address"},{"name":"valNullable","kind":"address","nullable":true}]},{"name":"test_bool","key_fields":[{"name":"key","kind":"bool"}],"value_fields":[{"name":"valNotNull","kind":"bool"},{"name":"valNullable","kind":"bool","nullable":true}]},{"name":"test_bytes","key_fields":[{"name":"key","kind":"bytes"}],"value_fields":[{"name":"valNotNull","kind":"bytes"},{"name":"valNullable","kind":"bytes","nullable":true}]},{"name":"test_decimal","key_fields":[{"name":"key","kind":"decimal"}],"value_fields":[{"name":"valNotNull","kind":"decimal"},{"name":"valNullable","kind":"decimal","nullable":true}]},{"name":"test_duration","key_fields":[{"name":"key","kind":"duration"}],"value_fields":[{"name":"valNotNull","kind":"duration"},{"name":"valNullable","kind":"duration","nullable":true}]},{"name":"test_enum","key_fields":[{"name":"key","kind":"enum","referenced_type":"test_enum_type"}],"value_fields":[{"name":"valNotNull","kind":"enum","referenced_type":"test_enum_type"},{"name":"valNullable","kind":"enum","nullable":true,"referenced_type":"test_enum_type"}]},{"name":"test_float32","key_fields":[{"name":"key","kind":"int32"}],"value_fields":[{"name":"valNotNull","kind":"float32"},{"name":"valNullable","kind":"float32","nullable":true}]},{"name":"test_float64","key_fields":[{"name":"key","kind":"int32"}],"value_fields":[{"name":"valNotNull","kind":"float64"},{"name":"valNullable","kind":"float64","nullable":true}]},{"name":"test_int16","key_fields":[{"name":"key","kind":"int16"}],"value_fields":[{"name":"valNotNull","kind":"int16"},{"name":"valNullable","kind":"int16","nullable":true}]},{"name":"test_int32","key_fields":[{"name":"key","kind":"int32"}],"value_fields":[{"name":"valNotNull","kind":"int32"},{"name":"valNullable","kind":"int32","nullable":true}]},{"name":"test_int64","key_fields":[{"name":"key","kind":"int64"}],"value_fields":[{"name":"valNotNull","kind":"int64"},{"name":"valNullable","kind":"int64","nullable":true}]},{"name":"test_int8","key_fields":[{"name":"key","kind":"int8"}],"value_fields":[{"name":"valNotNull","kind":"int8"},{"name":"valNullable","kind":"int8","nullable":true}]},{"name":"test_integer","key_fields":[{"name":"key","kind":"integer"}],"value_fields":[{"name":"valNotNull","kind":"integer"},{"name":"valNullable","kind":"integer","nullable":true}]},{"name":"test_string","key_fields":[{"name":"key","kind":"string"}],"value_fields":[{"name":"valNotNull","kind":"string"},{"name":"valNullable","kind":"string","nullable":true}]},{"name":"test_time","key_fields":[{"name":"key","kind":"time"}],"value_fields":[{"name":"valNotNull","kind":"time"},{"name":"valNullable","kind":"time","nullable":true}]},{"name":"test_uint16","key_fields":[{"name":"key","kind":"uint16"}],"value_fields":[{"name":"valNotNull","kind":"uint16"},{"name":"valNullable","kind":"uint16","nullable":true}]},{"name":"test_uint32","key_fields":[{"name":"key","kind":"uint32"}],"value_fields":[{"name":"valNotNull","kind":"uint32"},{"name":"valNullable","kind":"uint32","nullable":true}]},{"name":"test_uint64","key_fields":[{"name":"key","kind":"uint64"}],"value_fields":[{"name":"valNotNull","kind":"uint64"},{"name":"valNullable","kind":"uint64","nullable":true}]},{"name":"test_uint8","key_fields":[{"name":"key","kind":"uint8"}],"value_fields":[{"name":"valNotNull","kind":"uint8"},{"name":"valNullable","kind":"uint8","nullable":true}]}],"enum_types":[{"name":"test_enum_type","values":[{"name":"foo","value":1},{"name":"bar","value":2},{"name":"baz","value":3}]}]}}
|
||||
InitializeModuleData: {"ModuleName":"test_cases","Schema":{"object_types":[{"name":"ManyValues","key_fields":[{"name":"Key","kind":"string"}],"value_fields":[{"name":"Value1","kind":"int32"},{"name":"Value2","kind":"bytes"},{"name":"Value3","kind":"float64"},{"name":"Value4","kind":"uint64"}]},{"name":"RetainDeletions","key_fields":[{"name":"Key","kind":"string"}],"value_fields":[{"name":"Value1","kind":"int32"},{"name":"Value2","kind":"bytes"}],"retain_deletions":true},{"name":"Simple","key_fields":[{"name":"Key","kind":"string"}],"value_fields":[{"name":"Value1","kind":"int32"},{"name":"Value2","kind":"bytes"}]},{"name":"Singleton","value_fields":[{"name":"Value","kind":"string"},{"name":"Value2","kind":"bytes"}]},{"name":"ThreeKeys","key_fields":[{"name":"Key1","kind":"string"},{"name":"Key2","kind":"int32"},{"name":"Key3","kind":"uint64"}],"value_fields":[{"name":"Value1","kind":"int32"}]},{"name":"TwoKeys","key_fields":[{"name":"Key1","kind":"string"},{"name":"Key2","kind":"int32"}]}],"enum_types":null}}
|
||||
StartBlock: {1 <nil> <nil>}
|
||||
OnObjectUpdate: {"ModuleName":"test_cases","Updates":[{"TypeName":"RetainDeletions","Key":"","Value":[4602,"NwsAtcME5moByAKKwXU="],"Delete":false},{"TypeName":"Simple","Key":"","Value":[-89,"fgY="],"Delete":false}]}
|
||||
OnObjectUpdate: {"ModuleName":"test_cases","Updates":[{"TypeName":"Singleton","Key":null,"Value":["֑Ⱥ|@!`",""],"Delete":false}]}
|
||||
|
||||
23
schema/testing/json_test.go
Normal file
23
schema/testing/json_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package schematesting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"pgregory.net/rapid"
|
||||
|
||||
"cosmossdk.io/schema"
|
||||
)
|
||||
|
||||
func TestModuleSchemaJSON(t *testing.T) {
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
modSchema := ModuleSchemaGen().Draw(t, "moduleSchema")
|
||||
bz, err := json.Marshal(modSchema)
|
||||
require.NoError(t, err)
|
||||
var modSchema2 schema.ModuleSchema
|
||||
err = json.Unmarshal(bz, &modSchema2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, modSchema, modSchema2)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user