mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package meta
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
)
|
|
|
|
// CanonicalType maps meta_data's non-standard type names to the standard
|
|
// JSON-Schema/type vocabulary used downstream (envelope render, flag kinds):
|
|
// "file" -> "string", "list" -> "array"; other types pass through unchanged.
|
|
func (f Field) CanonicalType() string {
|
|
switch f.Type {
|
|
case "file":
|
|
return "string"
|
|
case "list":
|
|
return "array"
|
|
default:
|
|
return f.Type
|
|
}
|
|
}
|
|
|
|
// coerceLiteral converts a meta_data literal (default/enum/example) to the
|
|
// field's canonical type. Literals may arrive as strings (meta_data's usual
|
|
// form) OR already typed — a JSON number unmarshals to float64, a JSON bool to
|
|
// bool — so both must be normalized to the SAME Go type the canonical type
|
|
// implies (int64 for "integer", float64 for "number", bool for "boolean").
|
|
// Otherwise enumLess, which type-asserts on that Go type, can't order the
|
|
// values. Returns (value, true) on success, (nil, false) when the literal
|
|
// cannot be represented in the declared type.
|
|
func coerceLiteral(canonicalType string, raw any) (any, bool) {
|
|
switch canonicalType {
|
|
case "integer":
|
|
switch v := raw.(type) {
|
|
case string:
|
|
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
|
return n, true
|
|
}
|
|
case float64: // JSON number; accept only when it's a whole value
|
|
if v == float64(int64(v)) {
|
|
return int64(v), true
|
|
}
|
|
case int64:
|
|
return v, true
|
|
case int:
|
|
return int64(v), true
|
|
}
|
|
return nil, false
|
|
case "number":
|
|
switch v := raw.(type) {
|
|
case string:
|
|
if n, err := strconv.ParseFloat(v, 64); err == nil {
|
|
return n, true
|
|
}
|
|
case float64:
|
|
return v, true
|
|
case int64:
|
|
return float64(v), true
|
|
case int:
|
|
return float64(v), true
|
|
}
|
|
return nil, false
|
|
case "boolean":
|
|
switch v := raw.(type) {
|
|
case string:
|
|
switch v {
|
|
case "true":
|
|
return true, true
|
|
case "false":
|
|
return false, true
|
|
}
|
|
case bool:
|
|
return v, true
|
|
}
|
|
return nil, false
|
|
default: // "string", "array", "" (objects), or unknown — pass through as-is
|
|
return raw, true
|
|
}
|
|
}
|
|
|
|
// enumLess orders two coerced enum values for the canonical type, so integer
|
|
// enums end up [1 2 10] not lexicographic [1 10 2].
|
|
func enumLess(canonicalType string, a, b any) bool {
|
|
switch canonicalType {
|
|
case "integer":
|
|
ai, _ := a.(int64)
|
|
bi, _ := b.(int64)
|
|
return ai < bi
|
|
case "number":
|
|
af, _ := a.(float64)
|
|
bf, _ := b.(float64)
|
|
return af < bf
|
|
case "boolean":
|
|
ab, _ := a.(bool)
|
|
bb, _ := b.(bool)
|
|
return !ab && bb
|
|
default:
|
|
as, _ := a.(string)
|
|
bs, _ := b.(string)
|
|
return as < bs
|
|
}
|
|
}
|
|
|
|
// EnumOption is one allowed value paired with its human description. The
|
|
// description comes from options[].description and is empty for the bare `enum`
|
|
// form (which carries no descriptions).
|
|
type EnumOption struct {
|
|
Value any
|
|
Description string
|
|
}
|
|
|
|
// EnumOptions returns the field's allowed values paired with their descriptions
|
|
// — from enum (with descriptions backfilled from options when the field carries
|
|
// both forms), or from options when enum is absent — coerced to the canonical
|
|
// type and ordered: numeric and boolean values are sorted; string values keep
|
|
// source order (which can encode priority). Uncoercible literals are dropped.
|
|
// Returns nil when the field declares no enum constraint.
|
|
func (f Field) EnumOptions() []EnumOption {
|
|
ct := f.CanonicalType()
|
|
var out []EnumOption
|
|
switch {
|
|
case len(f.Enum) > 0:
|
|
// key by raw literal so enum "1" and option 1 align across JSON types
|
|
desc := make(map[string]string, len(f.Options))
|
|
for _, o := range f.Options {
|
|
desc[fmt.Sprintf("%v", o.Value)] = o.Description
|
|
}
|
|
for _, e := range f.Enum {
|
|
if v, ok := coerceLiteral(ct, e); ok {
|
|
out = append(out, EnumOption{Value: v, Description: desc[fmt.Sprintf("%v", e)]})
|
|
}
|
|
}
|
|
case len(f.Options) > 0:
|
|
seen := make(map[string]bool)
|
|
for _, o := range f.Options {
|
|
key := fmt.Sprintf("%v", o.Value)
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
if v, ok := coerceLiteral(ct, o.Value); ok {
|
|
out = append(out, EnumOption{Value: v, Description: o.Description})
|
|
}
|
|
}
|
|
}
|
|
if len(out) > 0 && ct != "string" && ct != "" {
|
|
sort.SliceStable(out, func(i, j int) bool { return enumLess(ct, out[i].Value, out[j].Value) })
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EnumValues returns the field's allowed values — the value projection of
|
|
// EnumOptions, in the same order. nil when the field declares no enum
|
|
// constraint. (Kept as the values-only accessor for the envelope and flag
|
|
// completion, which don't need descriptions.)
|
|
func (f Field) EnumValues() []any {
|
|
opts := f.EnumOptions()
|
|
if len(opts) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]any, len(opts))
|
|
for i, o := range opts {
|
|
out[i] = o.Value
|
|
}
|
|
return out
|
|
}
|
|
|
|
// CoercedDefault returns Default coerced to the canonical type, or nil when the
|
|
// field has no default or the literal cannot be coerced.
|
|
func (f Field) CoercedDefault() any { return f.coerce(f.Default) }
|
|
|
|
// CoercedExample returns Example coerced to the canonical type, or nil when the
|
|
// field has no example or the literal cannot be coerced.
|
|
func (f Field) CoercedExample() any { return f.coerce(f.Example) }
|
|
|
|
func (f Field) coerce(raw any) any {
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
if v, ok := coerceLiteral(f.CanonicalType(), raw); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MinBound returns the field's min constraint as a number, or nil when absent
|
|
// or unparseable. meta_data carries min/max as strings and does not say
|
|
// whether they bound a value or a string's length; the accessors stay equally
|
|
// agnostic, so every renderer (envelope minimum/maximum, flag help) presents
|
|
// the same numbers without inventing a semantic the source doesn't declare.
|
|
func (f Field) MinBound() *float64 { return parseBound(f.Min) }
|
|
|
|
// MaxBound returns the field's max constraint as a number, or nil when absent
|
|
// or unparseable. See MinBound.
|
|
func (f Field) MaxBound() *float64 { return parseBound(f.Max) }
|
|
|
|
func parseBound(s string) *float64 {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
if v, err := strconv.ParseFloat(s, 64); err == nil {
|
|
return &v
|
|
}
|
|
return nil
|
|
}
|