cosmos-sdk/schema/kind.go
Aaron Craelius 73ee336359
feat(schema): add API descriptors, struct, oneof & list types, and wire encoding spec (#21482)
Co-authored-by: marbar3778 <marbar3778@yahoo.com>
Co-authored-by: Marko <marko@baricevic.me>
2024-10-01 07:40:42 +00:00

579 lines
20 KiB
Go

package schema
import (
"encoding/json"
"fmt"
"regexp"
"time"
"unicode/utf8"
)
// Kind represents the basic type of a field in an object.
// Each kind defines the following encodings:
//
// - Go Encoding: the golang type which should be accepted by listeners and
// generated by decoders when providing entity updates.
// - JSON Encoding: the JSON encoding which should be used when encoding the field to JSON.
// - Key Binary Encoding: the encoding which should be used when encoding the field
// as a key in binary messages. Some encodings specify a terminal and non-terminal form
// depending on whether or not the field is the last field in the key.
// - Value Binary Encoding: the encoding which should be used when encoding the field
// as a value in binary messages.
//
// When there is some non-determinism in an encoding, kinds should specify what
// values they accept and also what is the canonical, deterministic encoding which
// should be preferably emitted by serializers.
//
// Binary encodings were chosen based on what is likely to be the most convenient default binary encoding
// for state management implementations. This encoding allows for sorted keys whenever it is possible for a kind
// and is deterministic.
// Modules that use the specified encoding natively will have a trivial decoder implementation because the
// encoding is already in the correct format after any initial prefix bytes are stripped.
type Kind int
const (
// InvalidKind indicates that an invalid type.
InvalidKind Kind = iota
// StringKind is a string type.
// Go Encoding: UTF-8 string with no null characters.
// JSON Encoding: string
// Key Binary Encoding:
// non-terminal: UTF-8 string with no null characters suffixed with a null character
// terminal: UTF-8 string with no null characters
// Value Binary Encoding: the same value binary encoding as BytesKind.
StringKind
// BytesKind represents a byte array.
// Go Encoding: []byte
// JSON Encoding: base64 encoded string, canonical values should be encoded with standard encoding and padding.
// Either standard or URL encoding with or without padding should be accepted.
// Key Binary Encoding:
// non-terminal: length prefixed bytes where the width of the length prefix is 1, 2, 3 or 4 bytes depending on
// the field's MaxLength (defaulting to 4 bytes).
// Length prefixes should be big-endian encoded.
// Values larger than 2^32 bytes are not supported (likely key-value stores impose a lower limit).
// terminal: raw bytes with no length prefix
// Value Binary Encoding: two 32-bit unsigned little-endian integers, the first one representing the offset of the
// value in the buffer and the second one representing the length of the value.
BytesKind
// Int8Kind represents an 8-bit signed integer.
// Go Encoding: int8
// JSON Encoding: number
// Key Binary Encoding: 1-byte two's complement encoding, with the first bit inverted for sorting.
// Value Binary Encoding: 1-byte two's complement encoding.
Int8Kind
// Uint8Kind represents an 8-bit unsigned integer.
// Go Encoding: uint8
// JSON Encoding: number
// Key Binary Encoding: 1-byte unsigned encoding.
// Value Binary Encoding: 1-byte unsigned encoding.
Uint8Kind
// Int16Kind represents a 16-bit signed integer.
// Go Encoding: int16
// JSON Encoding: number
// Key Binary Encoding: 2-byte two's complement big-endian encoding, with the first bit inverted for sorting.
// Value Binary Encoding: 2 byte two's complement little-endian encoding.
Int16Kind
// Uint16Kind represents a 16-bit unsigned integer.
// Go Encoding: uint16
// JSON Encoding: number
// Key Binary Encoding: 2-byte unsigned big-endian encoding.
// Value Binary Encoding: 2-byte unsigned little-endian encoding.
Uint16Kind
// Int32Kind represents a 32-bit signed integer.
// Go Encoding: int32
// JSON Encoding: number
// Key Binary Encoding: 4-byte two's complement big-endian encoding, with the first bit inverted for sorting.
// Value Binary Encoding: 4-byte two's complement little-endian encoding.
Int32Kind
// Uint32Kind represents a 32-bit unsigned integer.
// Go Encoding: uint32
// JSON Encoding: number
// Key Binary Encoding: 4-byte unsigned big-endian encoding.
// Value Binary Encoding: 4-byte unsigned little-endian encoding.
Uint32Kind
// Int64Kind represents a 64-bit signed integer.
// Go Encoding: int64
// JSON Encoding: base10 integer string which matches the IntegerFormat regex
// The canonical encoding should include no leading zeros.
// Key Binary Encoding: 8-byte two's complement big-endian encoding, with the first bit inverted for sorting.
// Value Binary Encoding: 8-byte two's complement little-endian encoding.
Int64Kind
// Uint64Kind represents a 64-bit unsigned integer.
// Go Encoding: uint64
// JSON Encoding: base10 integer string which matches the IntegerFormat regex
// Canonically encoded values should include no leading zeros.
// Key Binary Encoding: 8-byte unsigned big-endian encoding.
// Value Binary Encoding: 8-byte unsigned little-endian encoding.
Uint64Kind
// IntegerKind represents an arbitrary precision integer number.
// Support for expressing the maximum bit precision of values will be added in the future.
// Go Encoding: string which matches the IntegerFormat regex (unstable, subject to change).
// JSON Encoding: base10 integer string
// Canonically encoded values should include no leading zeros.
// Equality comparison with integers should be done using numerical equality rather
// than string equality.
IntegerKind
// DecimalKind represents an arbitrary precision decimal or integer number.
// Support for optionally limiting the precision may be added in the future.
// Go Encoding: string which matches the DecimalFormat regex
// JSON Encoding: base10 decimal string
// Canonically encoded values should include no leading zeros or trailing zeros,
// and exponential notation with a lowercase 'e' should be used for any numbers
// with an absolute value less than or equal to 1e-6 or greater than or equal to 1e6.
// Equality comparison with decimals should be done using numerical equality rather
// than string equality.
DecimalKind
// BoolKind represents a boolean true or false value.
// Go Encoding: bool
// JSON Encoding: boolean
// Key Binary Encoding: 1-byte encoding where 0 is false and 1 is true.
// Value Binary Encoding: 1-byte encoding where 0 is false and 1 is true.
BoolKind
// TimeKind represents a nanosecond precision UNIX time value (with zero representing January 1, 1970 UTC).
// Its valid range is +/- 2^63 (the range of a 64-bit signed integer).
// Go Encoding: time.Time
// JSON Encoding: Any value IS0 8601 time stamp should be accepted.
// Canonical values should be encoded with UTC time zone Z, nanoseconds should
// be encoded with no trailing zeros, and T time values should always be present
// even at 00:00:00.
// Key Binary Encoding: 8-byte two's complement big-endian encoding, with the first bit inverted for sorting.
// Value Binary Encoding: 8-byte two's complement little-endian encoding.
TimeKind
// DurationKind represents the elapsed time between two nanosecond precision time values.
// Its valid range is +/- 2^63 (the range of a 64-bit signed integer).
// Go Encoding: time.Duration
// JSON Encoding: the number of seconds as a decimal string with no trailing zeros followed by
// a lowercase 's' character to represent seconds.
// Key Binary Encoding: 8-byte two's complement big-endian encoding, with the first bit inverted for sorting.
// Value Binary Encoding: 8-byte two's complement little-endian encoding.
DurationKind
// Float32Kind represents an IEEE-754 32-bit floating point number.
// Go Encoding: float32
// JSON Encoding: number
// Key Binary Encoding: 4-byte IEEE-754 encoding.
// Value Binary Encoding: 4-byte IEEE-754 encoding.
Float32Kind
// Float64Kind represents an IEEE-754 64-bit floating point number.
// Go Encoding: float64
// JSON Encoding: number
// Key Binary Encoding: 8-byte IEEE-754 encoding.
// Value Binary Encoding: 8-byte IEEE-754 encoding.
Float64Kind
// AddressKind represents an account address which is represented by a variable length array of bytes.
// Addresses usually have a human-readable rendering, such as bech32, and tooling should provide
// a way for apps to define a string encoder for friendly user-facing display. Addresses have a maximum
// supported length of 63 bytes.
// Go Encoding: []byte
// JSON Encoding: addresses should be encoded as strings using the human-readable address renderer
// provided to the JSON encoder.
// Key Binary Encoding:
// non-terminal: bytes prefixed with 1-byte length prefix
// terminal: raw bytes with no length prefix
// Value Binary Encoding: bytes prefixed with 1-byte length prefix.
AddressKind
// EnumKind represents a value of an enum type.
// Fields of this type are expected to set the EnumType field in the field definition to the enum
// definition.
// Go Encoding: string
// JSON Encoding: string
// Key Binary Encoding: the same binary encoding as the EnumType's numeric kind.
// Value Binary Encoding: the same binary encoding as the EnumType's numeric kind.
EnumKind
// JSONKind represents arbitrary JSON data.
// Go Encoding: json.RawMessage
// JSON Encoding: any valid JSON value
// Key Binary Encoding: string encoding
// Value Binary Encoding: string encoding
JSONKind
// UIntNKind represents a signed integer type with a width in bits specified by the Size field in the
// field definition.
// Support for this is currently UNIMPLEMENTED, this notice will be removed when it is added.
// N must be a multiple of 8, and it is invalid for N to equal 8, 16, 32, 64 as there are more specific
// types for these widths.
// Go Encoding: []byte where len([]byte) == Size / 8, little-endian encoded.
// JSON Encoding: base10 integer string matching the IntegerFormat regex, canonically with no leading zeros.
// Key Binary Encoding: N / 8 bytes big-endian encoded
// Value Binary Encoding: N / 8 bytes little-endian encoded
UIntNKind
// IntNKind represents an unsigned integer type with a width in bits specified by the Size field in the
// field definition. N must be a multiple of 8.
// Support for this is currently UNIMPLEMENTED, this notice will be removed when it is added.
// N must be a multiple of 8, and it is invalid for N to equal 8, 16, 32, 64 as there are more specific
// types for these widths.
// Go Encoding: []byte where len([]byte) == Size / 8, two's complement little-endian encoded.
// JSON Encoding: base10 integer string matching the IntegerFormat regex, canonically with no leading zeros.
// Key Binary Encoding: N / 8 bytes big-endian two's complement encoded with the first bit inverted for sorting.
// Value Binary Encoding: N / 8 bytes little-endian two's complement encoded.
IntNKind
// StructKind represents a struct object.
// Support for this is currently UNIMPLEMENTED, this notice will be removed when it is added.
// Go Encoding: an array of type []interface{} where each element is of the respective field's kind type.
// JSON Encoding: an object where each key is the field name and the value is the field value.
// Canonically, keys are in alphabetical order with no extra whitespace.
// Key Binary Encoding: not valid as a key field.
// Value Binary Encoding: 32-bit unsigned little-endian length prefix,
// followed by the value binary encoding of each field in order.
StructKind
// OneOfKind represents a field that can be one of a set of types.
// Support for this is currently UNIMPLEMENTED, this notice will be removed when it is added.
// Go Encoding: the anonymous struct { Case string; Value interface{} }, aliased as OneOfValue.
// JSON Encoding: same as the case's struct encoding with "@type" set to the case name.
// Key Binary Encoding: not valid as a key field.
// Value Binary Encoding: the oneof's discriminant numeric value encoded as its discriminant kind
// followed by the encoded value.
OneOfKind
// ListKind represents a list of elements.
// Support for this is currently UNIMPLEMENTED, this notice will be removed when it is added.
// Go Encoding: an array of type []interface{} where each element is of the respective field's kind type.
// JSON Encoding: an array of values where each element is the field value.
// Canonically, there is no extra whitespace.
// Key Binary Encoding: not valid as a key field.
// Value Binary Encoding: 32-bit unsigned little-endian size prefix indicating the size of the encoded data in bytes,
// followed by a 32-bit unsigned little-endian count of the number of elements in the list,
// followed by each element encoded with value binary encoding.
ListKind
)
// MAX_VALID_KIND is the maximum valid kind value.
const MAX_VALID_KIND = JSONKind
const (
// IntegerFormat is a regex that describes the format integer number strings must match. It specifies
// that integers may have at most 100 digits.
IntegerFormat = `^-?[0-9]{1,100}$`
// DecimalFormat is a regex that describes the format decimal number strings must match. It specifies
// that decimals may have at most 50 digits before and after the decimal point and may have an optional
// exponent of up to 2 digits. These restrictions ensure that the decimal can be accurately represented
// by a wide variety of implementations.
DecimalFormat = `^-?[0-9]{1,50}(\.[0-9]{1,50})?([eE][-+]?[0-9]{1,2})?$`
)
// Validate returns an errContains if the kind is invalid.
func (t Kind) Validate() error {
if t <= InvalidKind {
return fmt.Errorf("unknown type: %d", t)
}
if t > JSONKind {
return fmt.Errorf("invalid type: %d", t)
}
return nil
}
// String returns a string representation of the kind.
func (t Kind) String() string {
switch t {
case StringKind:
return "string"
case BytesKind:
return "bytes"
case Int8Kind:
return "int8"
case Uint8Kind:
return "uint8"
case Int16Kind:
return "int16"
case Uint16Kind:
return "uint16"
case Int32Kind:
return "int32"
case Uint32Kind:
return "uint32"
case Int64Kind:
return "int64"
case Uint64Kind:
return "uint64"
case DecimalKind:
return "decimal"
case IntegerKind:
return "integer"
case BoolKind:
return "bool"
case TimeKind:
return "time"
case DurationKind:
return "duration"
case Float32Kind:
return "float32"
case Float64Kind:
return "float64"
case AddressKind:
return "address"
case EnumKind:
return "enum"
case JSONKind:
return "json"
default:
return fmt.Sprintf("invalid(%d)", t)
}
}
// ValidateValueType returns an errContains if the value does not conform to the expected go type.
// Some fields may accept nil values, however, this method does not have any notion of
// nullability. This method only validates that the go type of the value is correct for the kind
// and does not validate string or json formats. Kind.ValidateValue does a more thorough validation
// of number and json string formatting.
func (t Kind) ValidateValueType(value interface{}) error {
switch t {
case StringKind:
_, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
case BytesKind:
_, ok := value.([]byte)
if !ok {
return fmt.Errorf("expected []byte, got %T", value)
}
case Int8Kind:
_, ok := value.(int8)
if !ok {
return fmt.Errorf("expected int8, got %T", value)
}
case Uint8Kind:
_, ok := value.(uint8)
if !ok {
return fmt.Errorf("expected uint8, got %T", value)
}
case Int16Kind:
_, ok := value.(int16)
if !ok {
return fmt.Errorf("expected int16, got %T", value)
}
case Uint16Kind:
_, ok := value.(uint16)
if !ok {
return fmt.Errorf("expected uint16, got %T", value)
}
case Int32Kind:
_, ok := value.(int32)
if !ok {
return fmt.Errorf("expected int32, got %T", value)
}
case Uint32Kind:
_, ok := value.(uint32)
if !ok {
return fmt.Errorf("expected uint32, got %T", value)
}
case Int64Kind:
_, ok := value.(int64)
if !ok {
return fmt.Errorf("expected int64, got %T", value)
}
case Uint64Kind:
_, ok := value.(uint64)
if !ok {
return fmt.Errorf("expected uint64, got %T", value)
}
case IntegerKind:
_, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
case DecimalKind:
_, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
case BoolKind:
_, ok := value.(bool)
if !ok {
return fmt.Errorf("expected bool, got %T", value)
}
case TimeKind:
_, ok := value.(time.Time)
if !ok {
return fmt.Errorf("expected time.Time, got %T", value)
}
case DurationKind:
_, ok := value.(time.Duration)
if !ok {
return fmt.Errorf("expected time.Duration, got %T", value)
}
case Float32Kind:
_, ok := value.(float32)
if !ok {
return fmt.Errorf("expected float32, got %T", value)
}
case Float64Kind:
_, ok := value.(float64)
if !ok {
return fmt.Errorf("expected float64, got %T", value)
}
case AddressKind:
_, ok := value.([]byte)
if !ok {
return fmt.Errorf("expected []byte, got %T", value)
}
case EnumKind:
_, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
case JSONKind:
_, ok := value.(json.RawMessage)
if !ok {
return fmt.Errorf("expected json.RawMessage, got %T", value)
}
default:
return fmt.Errorf("invalid type: %d", t)
}
return nil
}
// ValidateValue returns an errContains if the value does not conform to the expected go type and format.
// It is more thorough, but slower, than Kind.ValidateValueType and validates that Integer, Decimal and JSON
// values are formatted correctly. It cannot validate enum values because Kind's do not have enum schemas.
func (t Kind) ValidateValue(value interface{}) error {
err := t.ValidateValueType(value)
if err != nil {
return err
}
switch t {
case StringKind:
str := value.(string)
if !utf8.ValidString(str) {
return fmt.Errorf("expected valid utf-8 string, got %s", value)
}
// check for null characters
for _, r := range str {
if r == 0 {
return fmt.Errorf("expected string without null characters, got %s", value)
}
}
case IntegerKind:
if !integerRegex.Match([]byte(value.(string))) {
return fmt.Errorf("expected base10 integer, got %s", value)
}
case DecimalKind:
if !decimalRegex.Match([]byte(value.(string))) {
return fmt.Errorf("expected decimal number, got %s", value)
}
case JSONKind:
if !json.Valid(value.(json.RawMessage)) {
return fmt.Errorf("expected valid JSON, got %s", value)
}
default:
return nil
}
return nil
}
// ValidKeyKind returns true if the kind is a valid key kind.
// All kinds except Float32Kind, Float64Kind, and JSONKind are valid key kinds
// because they do not define a strict form of equality.
func (t Kind) ValidKeyKind() bool {
switch t {
case Float32Kind, Float64Kind, JSONKind:
return false
default:
return true
}
}
var (
integerRegex = regexp.MustCompile(IntegerFormat)
decimalRegex = regexp.MustCompile(DecimalFormat)
)
// KindForGoValue finds the simplest kind that can represent the given go value. It will not, however,
// return kinds such as IntegerKind, DecimalKind, AddressKind, or EnumKind which all can be
// represented as strings.
func KindForGoValue(value interface{}) Kind {
switch value.(type) {
case string:
return StringKind
case []byte:
return BytesKind
case int8:
return Int8Kind
case uint8:
return Uint8Kind
case int16:
return Int16Kind
case uint16:
return Uint16Kind
case int32:
return Int32Kind
case uint32:
return Uint32Kind
case int64:
return Int64Kind
case uint64:
return Uint64Kind
case float32:
return Float32Kind
case float64:
return Float64Kind
case bool:
return BoolKind
case time.Time:
return TimeKind
case time.Duration:
return DurationKind
case json.RawMessage:
return JSONKind
default:
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
}
}