mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
216 lines
7.5 KiB
Go
216 lines
7.5 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package meta is the typed model of the API metadata registry and the single
|
|
// place that parses it. The metadata is a fixed, regular vocabulary, so a plain
|
|
// typed json.Unmarshal replaces hand-rolled map[string]interface{} walking. Map
|
|
// key order is not preserved (Go maps are unordered); callers that need a
|
|
// deterministic sequence get fields/methods/resources sorted by name via the
|
|
// list accessors below.
|
|
package meta
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Option is one enum option of a field. Value is `any` (not string) so a
|
|
// metadata value that arrives as a JSON number — rather than the usual quoted
|
|
// string — coerces like Field.Enum / EnumOption.Value instead of failing the
|
|
// whole registry unmarshal and blanking the entire catalog. coerceLiteral
|
|
// normalizes it to the field's declared type.
|
|
type Option struct {
|
|
Value any `json:"value"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// Field is one parameter or body/response field. Name is the parent map key,
|
|
// populated by the list accessors (not a JSON field). ref/annotations/enumName
|
|
// exist in the metadata but are intentionally not modeled (unused downstream).
|
|
type Field struct {
|
|
Name string `json:"-"`
|
|
Type string `json:"type"`
|
|
Location string `json:"location"` // "path" | "query"; empty for body/response
|
|
Required bool `json:"required"`
|
|
Description string `json:"description"`
|
|
Default any `json:"default"`
|
|
Example any `json:"example"`
|
|
Min string `json:"min"`
|
|
Max string `json:"max"`
|
|
Enum []any `json:"enum"`
|
|
Options []Option `json:"options"`
|
|
Properties map[string]Field `json:"properties"`
|
|
}
|
|
|
|
// FlagName is the kebab-case CLI flag for this field (chat_id -> chat-id).
|
|
func (f Field) FlagName() string { return strings.ReplaceAll(f.Name, "_", "-") }
|
|
|
|
// Children returns the field's nested properties sorted by name.
|
|
func (f Field) Children() []Field { return fieldsOf(f.Properties, nil) }
|
|
|
|
// Method is one API operation. Name is the parent map key. Affordance is kept
|
|
// raw so this package stays free of envelope concerns.
|
|
type Method struct {
|
|
Name string `json:"-"`
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
HTTPMethod string `json:"httpMethod"`
|
|
Description string `json:"description"`
|
|
Risk string `json:"risk"`
|
|
DocURL string `json:"docUrl"`
|
|
Danger bool `json:"danger"`
|
|
Tips []string `json:"tips"`
|
|
Scopes []string `json:"scopes"`
|
|
RequiredScopes []string `json:"requiredScopes"`
|
|
AccessTokens []Token `json:"accessTokens"`
|
|
Affordance json.RawMessage `json:"affordance"`
|
|
Parameters map[string]Field `json:"parameters"`
|
|
RequestBody map[string]Field `json:"requestBody"`
|
|
ResponseBody map[string]Field `json:"responseBody"`
|
|
}
|
|
|
|
// Params are the path/query parameters, sorted by name.
|
|
func (m Method) Params() []Field {
|
|
return fieldsOf(m.Parameters, func(f Field) bool {
|
|
return f.Location == "path" || f.Location == "query"
|
|
})
|
|
}
|
|
|
|
// Data are the non-file request-body fields (--data JSON), sorted by name.
|
|
func (m Method) Data() []Field {
|
|
return fieldsOf(m.RequestBody, func(f Field) bool { return f.Type != "file" })
|
|
}
|
|
|
|
// Files are the file-typed request-body fields (--file uploads), sorted by name.
|
|
func (m Method) Files() []Field {
|
|
return fieldsOf(m.RequestBody, func(f Field) bool { return f.Type == "file" })
|
|
}
|
|
|
|
// Response are the response-body fields, sorted by name.
|
|
func (m Method) Response() []Field { return fieldsOf(m.ResponseBody, nil) }
|
|
|
|
// fieldsOf materializes a name->field map into a name-injected slice, optionally
|
|
// filtered, sorted by name for deterministic output.
|
|
func fieldsOf(byName map[string]Field, keep func(Field) bool) []Field {
|
|
out := make([]Field, 0, len(byName))
|
|
for name, f := range byName {
|
|
f.Name = name
|
|
if keep == nil || keep(f) {
|
|
out = append(out, f)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// Resource groups methods (and may nest sub-resources). Name is the parent key.
|
|
type Resource struct {
|
|
Name string `json:"-"`
|
|
Methods map[string]Method `json:"methods"`
|
|
Resources map[string]Resource `json:"resources"`
|
|
}
|
|
|
|
// MethodList returns the resource's methods, name-injected and sorted by name.
|
|
func (r Resource) MethodList() []Method {
|
|
out := make([]Method, 0, len(r.Methods))
|
|
for name, m := range r.Methods {
|
|
m.Name = name
|
|
out = append(out, m)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// Method looks up one method by name with Name injected, or false if absent.
|
|
// Use this instead of indexing Methods directly so Name is never left empty.
|
|
func (r Resource) Method(name string) (Method, bool) {
|
|
m, ok := r.Methods[name]
|
|
if !ok {
|
|
return Method{}, false
|
|
}
|
|
m.Name = name
|
|
return m, true
|
|
}
|
|
|
|
// SubResources returns nested resources, name-injected and sorted by name.
|
|
func (r Resource) SubResources() []Resource { return resourcesOf(r.Resources) }
|
|
|
|
// Service is one API service. Name is a real JSON field (services is an array).
|
|
type Service struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
ServicePath string `json:"servicePath"`
|
|
Resources map[string]Resource `json:"resources"`
|
|
}
|
|
|
|
// ResourceList returns the service's top-level resources, name-injected and
|
|
// sorted by name.
|
|
func (s Service) ResourceList() []Resource { return resourcesOf(s.Resources) }
|
|
|
|
// Resource looks up one (possibly dotted) resource by name with Name injected,
|
|
// or false if absent. Use this instead of indexing Resources directly.
|
|
func (s Service) Resource(name string) (Resource, bool) {
|
|
r, ok := s.Resources[name]
|
|
if !ok {
|
|
return Resource{}, false
|
|
}
|
|
r.Name = name
|
|
return r, true
|
|
}
|
|
|
|
func resourcesOf(byName map[string]Resource) []Resource {
|
|
out := make([]Resource, 0, len(byName))
|
|
for name, r := range byName {
|
|
r.Name = name
|
|
out = append(out, r)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// Registry is the top-level metadata document.
|
|
type Registry struct {
|
|
Services []Service `json:"services"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// Parse decodes the metadata JSON into the typed Registry. Returns a zero
|
|
// Registry for empty input.
|
|
func Parse(data []byte) (Registry, error) {
|
|
if len(data) == 0 {
|
|
return Registry{}, nil
|
|
}
|
|
var reg Registry
|
|
if err := json.Unmarshal(data, ®); err != nil {
|
|
return Registry{}, err
|
|
}
|
|
return reg, nil
|
|
}
|
|
|
|
// FromMap decodes a single method spec from its map form into a typed Method.
|
|
// Convenience constructor for building typed values from map literals (tests).
|
|
func FromMap(method map[string]interface{}) Method {
|
|
b, err := json.Marshal(method)
|
|
if err != nil {
|
|
return Method{}
|
|
}
|
|
var m Method
|
|
_ = json.Unmarshal(b, &m)
|
|
return m
|
|
}
|
|
|
|
// ServiceFromMap decodes a service spec from its map form into a typed Service.
|
|
// Convenience constructor for building typed values from map literals (tests).
|
|
func ServiceFromMap(svc map[string]interface{}) Service {
|
|
b, err := json.Marshal(svc)
|
|
if err != nil {
|
|
return Service{}
|
|
}
|
|
var s Service
|
|
_ = json.Unmarshal(b, &s)
|
|
return s
|
|
}
|