Files
larksuite-cli/internal/meta/normalize_test.go

194 lines
8.2 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package meta
import (
"reflect"
"testing"
)
func TestField_CanonicalType(t *testing.T) {
cases := map[string]string{
"file": "string", // meta_data's non-standard "file" is a string with binary format
"list": "array", // "list" is meta_data's spelling of a JSON array
"integer": "integer",
"boolean": "boolean",
"string": "string",
"": "",
}
for in, want := range cases {
if got := (Field{Type: in}).CanonicalType(); got != want {
t.Errorf("CanonicalType(%q) = %q, want %q", in, got, want)
}
}
}
func TestField_EnumValues(t *testing.T) {
// string enum keeps source order (order can encode priority)
if got := (Field{Type: "string", Enum: []any{"b", "a", "c"}}).EnumValues(); !reflect.DeepEqual(got, []any{"b", "a", "c"}) {
t.Errorf("string enum = %v, want source order [b a c]", got)
}
// integer enum: string-stored literals coerced to int64 and numerically sorted
if got := (Field{Type: "integer", Enum: []any{"10", "2", "1"}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2), int64(10)}) {
t.Errorf("integer enum = %v, want [1 2 10] coerced+sorted", got)
}
// options used when enum absent, deduped, source order for strings
if got := (Field{Type: "string", Options: []Option{{Value: "x"}, {Value: "x"}, {Value: "y"}}}).EnumValues(); !reflect.DeepEqual(got, []any{"x", "y"}) {
t.Errorf("options enum = %v, want [x y] deduped", got)
}
// uncoercible literal dropped
if got := (Field{Type: "integer", Enum: []any{"1", "nope", "2"}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2)}) {
t.Errorf("bad enum = %v, want [1 2] (nope dropped)", got)
}
// no enum/options -> nil
if got := (Field{Type: "string"}).EnumValues(); got != nil {
t.Errorf("empty enum = %v, want nil", got)
}
}
func TestField_EnumOptions(t *testing.T) {
// options carry descriptions, kept paired with their (string) value in source order
fo := Field{Type: "string", Options: []Option{
{Value: "open_id", Description: "以 open_id 标识"},
{Value: "open_id", Description: "dup ignored"}, // dedup keeps first
{Value: "user_id", Description: "以 user_id 标识"},
}}
got := fo.EnumOptions()
want := []EnumOption{
{Value: "open_id", Description: "以 open_id 标识"},
{Value: "user_id", Description: "以 user_id 标识"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("EnumOptions = %+v, want %+v", got, want)
}
// integer enum (bare form): values coerced + numerically sorted, no descriptions
fi := Field{Type: "integer", Enum: []any{"10", "2", "1"}}
gi := fi.EnumOptions()
if len(gi) != 3 || gi[0].Value != int64(1) || gi[2].Value != int64(10) || gi[0].Description != "" {
t.Errorf("EnumOptions(integer) = %+v, want [1 2 10] coerced+sorted, no desc", gi)
}
// EnumValues stays the value projection of EnumOptions (golden-critical)
if !reflect.DeepEqual(fo.EnumValues(), []any{"open_id", "user_id"}) {
t.Errorf("EnumValues diverged from EnumOptions values: %v", fo.EnumValues())
}
// unconstrained -> nil
if (Field{Type: "string"}).EnumOptions() != nil {
t.Error("EnumOptions should be nil when unconstrained")
}
}
func TestField_EnumOptions_BothEnumAndOptions(t *testing.T) {
// enum is the value set; descriptions backfilled from options, empty where absent
f := Field{Type: "string", Enum: []any{"1", "2", "3", "4", "6"}, Options: []Option{
{Value: "1", Description: "from"},
{Value: "2", Description: "to"},
{Value: "6", Description: "subject"},
}}
want := []EnumOption{
{Value: "1", Description: "from"},
{Value: "2", Description: "to"},
{Value: "3", Description: ""},
{Value: "4", Description: ""},
{Value: "6", Description: "subject"},
}
if got := f.EnumOptions(); !reflect.DeepEqual(got, want) {
t.Errorf("EnumOptions(enum+options) = %+v, want %+v", got, want)
}
// enum values stored as strings match option values stored as numbers
fi := Field{Type: "integer", Enum: []any{"10", "2", "1"}, Options: []Option{
{Value: 1, Description: "one"},
{Value: 2, Description: "two"},
}}
wantI := []EnumOption{
{Value: int64(1), Description: "one"},
{Value: int64(2), Description: "two"},
{Value: int64(10), Description: ""},
}
if got := fi.EnumOptions(); !reflect.DeepEqual(got, wantI) {
t.Errorf("EnumOptions(integer enum+options) = %+v, want %+v", got, wantI)
}
}
func TestField_Enum_NumberAndBoolean(t *testing.T) {
// number: string-stored floats coerced to float64 and numerically sorted
if got := (Field{Type: "number", Enum: []any{"2.5", "1.5", "10"}}).EnumValues(); !reflect.DeepEqual(got, []any{1.5, 2.5, float64(10)}) {
t.Errorf("number enum = %v, want [1.5 2.5 10] coerced+sorted", got)
}
// number: uncoercible literal dropped
if got := (Field{Type: "number", Enum: []any{"1.5", "x", "2.5"}}).EnumValues(); !reflect.DeepEqual(got, []any{1.5, 2.5}) {
t.Errorf("number enum with bad value = %v, want [1.5 2.5]", got)
}
// boolean: true/false coerced and sorted (false before true); invalid dropped
if got := (Field{Type: "boolean", Enum: []any{"true", "maybe", "false"}}).EnumValues(); !reflect.DeepEqual(got, []any{false, true}) {
t.Errorf("boolean enum = %v, want [false true]", got)
}
}
func TestField_EnumOptions_NonStringValuesNormalized(t *testing.T) {
// JSON numbers/bools arrive already-typed (a number is float64, not a
// string) — e.g. options[].value: 0. They must still be normalized to the
// field's canonical type (int64 for "integer") and sorted numerically;
// leaving them as float64 both yields the wrong type and defeats enumLess,
// whose integer branch asserts int64 and would otherwise treat every value
// as zero (no sort).
if got := (Field{Type: "integer", Options: []Option{
{Value: float64(10)}, {Value: float64(2)}, {Value: float64(1)},
}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2), int64(10)}) {
t.Errorf("integer options from float64 = %#v, want [int64(1) int64(2) int64(10)]", got)
}
// bare enum form, JSON numbers
if got := (Field{Type: "integer", Enum: []any{float64(3), float64(1), float64(2)}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(2), int64(3)}) {
t.Errorf("integer enum from float64 = %#v, want [int64(1) int64(2) int64(3)]", got)
}
// number field: a whole-valued float stays float64
if got := (Field{Type: "number", Enum: []any{float64(2), float64(1)}}).EnumValues(); !reflect.DeepEqual(got, []any{float64(1), float64(2)}) {
t.Errorf("number enum from float64 = %#v, want [float64(1) float64(2)]", got)
}
// boolean field: native bools coerce + sort (false < true)
if got := (Field{Type: "boolean", Enum: []any{true, false}}).EnumValues(); !reflect.DeepEqual(got, []any{false, true}) {
t.Errorf("boolean enum from bool = %#v, want [false true]", got)
}
// non-integral float under integer is uncoercible -> dropped (mirrors how "2.5" fails ParseInt)
if got := (Field{Type: "integer", Enum: []any{float64(1), float64(2.5), float64(3)}}).EnumValues(); !reflect.DeepEqual(got, []any{int64(1), int64(3)}) {
t.Errorf("integer enum with fractional float = %#v, want [int64(1) int64(3)]", got)
}
}
func TestField_CoercedDefaultAndExample(t *testing.T) {
if got := (Field{Type: "integer", Default: "5"}).CoercedDefault(); got != int64(5) {
t.Errorf("CoercedDefault integer = %v (%T), want int64(5)", got, got)
}
if got := (Field{Type: "integer", Default: "bad"}).CoercedDefault(); got != nil {
t.Errorf("CoercedDefault uncoercible = %v, want nil", got)
}
if got := (Field{Type: "string"}).CoercedDefault(); got != nil {
t.Errorf("CoercedDefault absent = %v, want nil", got)
}
if got := (Field{Type: "boolean", Example: "true"}).CoercedExample(); got != true {
t.Errorf("CoercedExample boolean = %v, want true", got)
}
}
func TestField_Bounds(t *testing.T) {
f := Field{Min: "1", Max: "100"}
if v := f.MinBound(); v == nil || *v != 1 {
t.Errorf("MinBound = %v, want 1", v)
}
if v := f.MaxBound(); v == nil || *v != 100 {
t.Errorf("MaxBound = %v, want 100", v)
}
if v := (Field{Min: "0.5"}).MinBound(); v == nil || *v != 0.5 {
t.Errorf("MinBound fractional = %v, want 0.5", v)
}
if v := (Field{}).MinBound(); v != nil {
t.Errorf("MinBound absent = %v, want nil", v)
}
if v := (Field{Max: "not_a_number"}).MaxBound(); v != nil {
t.Errorf("MaxBound unparseable = %v, want nil", v)
}
}