Files
larksuite-cli/internal/schema/types.go

181 lines
6.0 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package schema
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"github.com/larksuite/cli/internal/meta"
)
// Envelope is the MCP Tool spec contract for a single API method command.
//
// The REST route (httpMethod/path) is deliberately NOT exposed: every
// schema-resolvable method already has a typed command, so the raw path would
// only tempt an agent toward the `api` escape hatch.
type Envelope struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema *InputSchema `json:"inputSchema"`
OutputSchema *OutputSchema `json:"outputSchema"`
Meta *Meta `json:"_meta"`
}
// InputSchema is JSON Schema Draft 2020-12 flattened.
//
// Required is intentionally rendered (no omitempty) so the envelope shape
// stays stable for AI consumers — an empty []string means "no required
// fields" rather than "schema is missing the field".
type InputSchema struct {
Type string `json:"type"`
Required []string `json:"required"`
Properties *OrderedProps `json:"properties"`
}
// OutputSchema wraps responseBody into a JSON Schema object.
type OutputSchema struct {
Type string `json:"type"`
Properties *OrderedProps `json:"properties"`
}
// Property is one field's JSON Schema shape, recursive.
//
// Required is used when Property describes a nested object (e.g. the
// "params" / "data" sub-objects inside inputSchema): it lists which keys
// inside that object's Properties are mandatory. Leaf fields ignore it.
type Property struct {
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
// Flag is the typed CLI flag a params property maps to (e.g. "--folder-id");
// absent on body/file fields, which travel via the section's Carrier.
Flag string `json:"flag,omitempty"`
// Carrier names the flag a whole inputSchema section travels on ("--data" /
// "--file"); empty on the params section, whose properties carry their Flag.
Carrier string `json:"carrier,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
// EnumDescriptions, when present, is parallel to Enum: the human meaning of
// each allowed value, in the same order. Omitted when no value carries a
// description. This is the widely-recognized JSON-Schema extension (VS Code,
// OpenAPI tooling) that lets an AI consumer learn what each enum value means
// without a second lookup.
EnumDescriptions []string `json:"enumDescriptions,omitempty"`
Default interface{} `json:"default,omitempty"`
Example interface{} `json:"example,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
Format string `json:"format,omitempty"`
Required []string `json:"required,omitempty"`
Properties *OrderedProps `json:"properties,omitempty"`
Items *Property `json:"items,omitempty"`
}
// Meta is the Lark-specific extension namespace.
type Meta struct {
EnvelopeVersion string `json:"envelope_version"`
Scopes []string `json:"scopes"`
RequiredScopes []string `json:"required_scopes"`
AccessTokens []string `json:"access_tokens"`
Danger bool `json:"danger"`
Risk string `json:"risk"`
DocURL string `json:"doc_url,omitempty"`
Affordance *meta.Affordance `json:"affordance,omitempty"`
}
// OrderedProps is map[string]Property with preserved key order on MarshalJSON.
// It is used wherever JSON output must reflect meta_data.json's natural field
// order rather than Go's default alphabetical map encoding.
type OrderedProps struct {
Order []string
Map map[string]Property
}
// Set adds or replaces a property, recording first-seen keys in Order so JSON
// output preserves insertion order. Re-setting an existing key updates its
// value without reordering. Centralizing mutation here keeps Order and Map from
// drifting out of sync.
func (o *OrderedProps) Set(key string, p Property) {
if o.Map == nil {
o.Map = make(map[string]Property)
}
if _, exists := o.Map[key]; !exists {
o.Order = append(o.Order, key)
}
o.Map[key] = p
}
// MarshalJSON emits keys in Order, not alphabetical. If Order is empty but
// Map has entries, fall back to alphabetical key order over Map so callers
// that only populated Map (no explicit ordering) still see their fields.
func (o *OrderedProps) MarshalJSON() ([]byte, error) {
if o == nil || (len(o.Order) == 0 && len(o.Map) == 0) {
return []byte("{}"), nil
}
keys := o.Order
if len(keys) == 0 {
keys = make([]string, 0, len(o.Map))
for k := range o.Map {
keys = append(keys, k)
}
sort.Strings(keys)
}
var buf bytes.Buffer
buf.WriteByte('{')
for i, k := range keys {
if i > 0 {
buf.WriteByte(',')
}
keyJSON, err := json.Marshal(k)
if err != nil {
return nil, fmt.Errorf("marshal key %q: %w", k, err)
}
buf.Write(keyJSON)
buf.WriteByte(':')
valJSON, err := json.Marshal(o.Map[k])
if err != nil {
return nil, fmt.Errorf("marshal value for %q: %w", k, err)
}
buf.Write(valJSON)
}
buf.WriteByte('}')
return buf.Bytes(), nil
}
// UnmarshalJSON parses an object preserving key order via json.Decoder.Token().
// Used for round-tripping in tests (and future golden update flows).
func (o *OrderedProps) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
tok, err := dec.Token()
if err != nil {
return err
}
if delim, ok := tok.(json.Delim); !ok || delim != '{' {
return fmt.Errorf("expected object, got %v", tok)
}
o.Order = nil
o.Map = make(map[string]Property)
for dec.More() {
keyTok, err := dec.Token()
if err != nil {
return err
}
key, ok := keyTok.(string)
if !ok {
return fmt.Errorf("expected string key, got %v", keyTok)
}
var prop Property
if err := dec.Decode(&prop); err != nil {
return err
}
o.Order = append(o.Order, key)
o.Map[key] = prop
}
if _, err := dec.Token(); err != nil {
return err
}
return nil
}