mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
* feat(schema): add envelope types and ordered properties container
* feat(schema): build meta_data.json key-order index for property ordering
* feat(schema): implement convertProperty with file/enum/range/nested handling
* feat(schema): build inputSchema with x-in / file binary / yes injection
* feat(schema): build outputSchema wrapping responseBody
* feat(schema): build _meta with scopes/risk/access_tokens normalization
* feat(schema): scaffold affordance overlay loader (PR-1 stub)
* feat(schema): wire up AssembleEnvelope main entry point
* feat(schema): parse dotted and space-separated path arguments
* feat(schema): batch envelope assembly with optional method filter
* feat(schema): implement L1-L3 envelope lint (structure/type/cross-field)
* feat(schema): measure L4 coverage and gate all envelopes through L1-L3
* feat(schema): add golden test harness with UPDATE_GOLDEN refresh
* test(schema): seed 20 golden envelopes covering edge cases
* feat(schema): output MCP envelope as default JSON, preserve pretty mode
Rewrites cmd/schema/schema.go so the default --format json branch emits
MCP-spec envelopes via schema.AssembleAll/AssembleService/AssembleEnvelope.
The legacy --format pretty branch is preserved verbatim and still uses
printServices / printResourceList / printMethodDetail.
Args max raised from 1 to 8 so the path can be supplied either as a single
dotted argument (im.reactions.list) or as space-separated segments
(im reactions list); both forms route through schema.ParsePath and produce
byte-identical output.
The completeSchemaPath function is extended to drive tab-completion for
both forms: legacy dotted prefix when len(args) == 0, and per-segment
resource/method completion when args already contains earlier segments.
BREAKING CHANGE: default JSON output shape changes from the raw meta_data
structure to an MCP envelope array/object. Existing scripts parsing the
old shape must either pin --format pretty or migrate to the new envelope
fields (name, description, inputSchema, outputSchema, _meta).
* test(schema): cover envelope JSON output, space-form path, yes injection
Replaces TestSchemaCmd_NoArgs with two variants reflecting the new default
shape: TestSchemaCmd_NoArgs_Pretty asserts the legacy "Available services"
text appears only under --format pretty, and TestSchemaCmd_NoArgs_JSON_IsArray
asserts the default JSON output parses as an envelope array with at least 180
entries.
Adds six new tests:
- TestSchemaCmd_JSONIsEnvelope: single-method output has name / description
/ inputSchema / outputSchema / _meta keys and envelope_version "1.0".
- TestSchemaCmd_SpaceSeparatedPath_EqualsDotted: dotted and space forms
produce identical output bytes for the same command path.
- TestSchemaCmd_ServiceListIsArray: schema <service> returns a JSON array
whose every entry's name starts with "<service> ".
- TestSchemaCmd_HighRiskYesInjection: high-risk-write commands inject
inputSchema.properties.yes.
- TestSchemaCmd_NoYesForReadRisk: read-risk commands do not inject yes.
- TestSchemaCmd_PrettyUnchanged_KeyTextPresent: --format pretty still
surfaces the legacy section markers (Parameters:, Response:, Identity:,
Scopes:, CLI:).
* feat(schema): assemble envelope from embedded data only for stability
* chore(schema): lint cleanup
* fix(schema): preserve dotted resource segments in envelope name
Nested resources whose meta_data key contains a dot (e.g. chat.members,
user_mailbox.templates) were previously split on '.' and rejoined with
spaces, producing envelope names like 'im chat members bots'. AI
consumers doing name.split(' ') and feeding the result back as argv
got 'lark-cli im chat members bots' which the CLI rejects — the actual
invocation form is 'lark-cli im chat.members bots'.
Pass the dotted resource key as a single argv segment so the envelope
name 'im chat.members bots' round-trips through name.split(' ') back
to the CLI. Mirror the same convention in the golden harness so its
single-method assembly matches the live AssembleService walk.
* fix(schema): align MCP envelope output with JSON Schema 2020-12 contract
- coerce enum literals to typed JSON values (integer to int64,
number to float64, boolean to bool) so type:"integer" fields no
longer emit string enums; sort numeric/boolean enums while
preserving meta_data order for string enums that carry semantic
priority
- translate non-standard meta_data type:"list" to JSON Schema
type:"array" with items:{} fallback when element shape is absent
(covers the two mail attachment_ids fields)
- render inputSchema.required even when empty so consumers see a
stable envelope shape ("[]" means no required fields, not "field
is missing")
- reject trailing path segments in both JSON and pretty modes so
schema im.messages.delete.foo errors instead of silently
returning the delete method
- drop dead "list type" entry from lint_test isKnownDataInconsistency
whitelist now that list values are translated upstream
* fix(schema): address CodeRabbit findings and stabilize CI tests
CI fix
- Replace hard-coded absolute key-order assertions in TestKeyOrderIndex_*
and TestBuildInputSchema_* with set-membership and propagation invariants;
the upstream meta_data API does not guarantee stable JSON key order across
fetches, so the old tests were flaky on CI by design.
- Skip byte-level TestGoldenEnvelopes when CI=true; golden snapshots are a
manual refresh artefact tied to a specific meta_data fetch, not a CI gate.
- Add TestMain to isolate registry-backed tests from any host ~/.lark-cli
cache (LARKSUITE_CLI_CONFIG_DIR + LARKSUITE_CLI_REMOTE_META=off) so the
suite gives the same answer on every machine.
CodeRabbit review actionables
- EmbeddedServiceNames returns a defensive copy so callers cannot mutate
the package-level slice and affect subsequent assembly determinism.
- coerceEnumValue is now also applied to default literals: integer fields
no longer ship default: "500" — they ship default: 500 (same idea as the
earlier enum coercion fix).
- options-branch string enums preserve meta_data source order, matching the
enum-branch policy; only numeric/boolean enums get sorted.
- validatePropertyTypes now validates the array element schema itself
(type, nested items), not only items.properties — previously a primitive
element with an invalid type (e.g. items.type="list") slipped past lint.
- OrderedProps.MarshalJSON falls back to alphabetical key order when Map
has entries but Order is empty, instead of silently emitting {}.
Tests pass locally and with CI=true env (simulating GitHub Actions).
* chore(schema): refresh golden envelopes after meta_data drift
Re-generated with UPDATE_GOLDEN=1 against the current meta_data.json
snapshot. The bulk of the diff is upstream noise (description wording,
enum entries, field order) which the CI snapshot diff can no longer
reasonably gate (see previous commit). Side-effects of the code fixes
in the parent commit are also captured:
- integer-typed defaults now emit numeric literals (e.g. page_size
default 500, not "500") thanks to coerceEnumValue
- mail.user_mailbox.templates.create _meta.risk corrects to "write"
(assembler already emitted "write"; the old golden was stale)
* fix(schema): address CodeRabbit round-3 review findings
- TestMain: cleanup now runs reliably. os.Exit skips deferred functions,
so the previous defer os.RemoveAll(dir) never executed. Replace defer
with explicit cleanup, and fail fast if MkdirTemp errors instead of
silently running against the host cache (which defeats isolation).
- convertProperty default coercion: when the literal cannot be coerced to
the declared type (e.g. default:"" on integer field, used by meta_data
to mean "no default"), omit the field entirely rather than emit a
type-mismatched default. Removes a contract violation flagged on
im.reactions.list.json#page_size.
* feat(schema): wire affordance overlay into envelope _meta
Replace the loadAffordance stub (which always returned nil and read
from an empty embedded annotations/ directory) with parseAffordance,
which lifts the affordance block from method["affordance"]. The block
is authored under larksuite-cli-registry's registry-config.yaml in the
overrides: section and flows through gen-registry.py's deep_merge into
the embedded meta_data.json.
Simplify buildMeta signature: the service/resourcePath/method args
existed only to feed the old dotted-path lookup.
Refresh 9 golden envelopes for unrelated upstream meta_data.json drift.
* refactor(schema): drop x-in extension from inputSchema
x-in (path/query/body) was an HTTP-shape leak in a CLI-facing tool spec.
AI consumers call the CLI by name with named args — they never construct
HTTP requests directly, so the path-vs-body-vs-query distinction is the
CLI's internal concern, not part of the contract.
Execution path (cmd/service/service.go) already reads location from
meta_data.json directly, so removing x-in does not affect routing.
Drop:
- Property.XIn field
- validXIn map and the two lint rules that depend on x-in
(L1 "top-level missing x-in" and L2 "path field must be in required")
- contains() helper, no longer referenced after the path-required rule
went away
Refresh 20 goldens for the now-absent x-in lines.
* refactor(schema): wrap inputSchema into params/data/flags sub-objects
Replace the flat inputSchema with a 3-bucket nested structure that mirrors
the CLI's actual flag layout, so AI consumers can directly map envelope
fields to lark-cli invocation:
inputSchema:
properties:
params: { ...path + query fields } → CLI --params JSON
data: { ...body fields } → CLI --data JSON
flags: { yes: ... } → CLI --yes (only for high-risk-write)
Each sub-object only appears when the method has the corresponding source,
so read-only GETs have a single `params` block, body-only POSTs have a
single `data` block, etc.
The `flags` wrapper carries an explicit description marking it as a CLI
control bucket (not API fields), so AI does not confuse `yes` with a
backend parameter.
Lint:
- L2 walkForL2 helper recurses into params/data sub-objects so leaf
invariants (format:binary on non-string, min<max, required-in-properties)
still apply.
- L3 yes-presence check now navigates flags.properties.yes.
Refresh all 20 goldens for the new shape.
* refactor(schema): drop flags wrapper, put yes at top level alongside params/data
The flags wrapper added one extra layer for a single field. Flatten so
inputSchema.properties has three siblings:
inputSchema:
properties:
params: { ...path + query } → CLI --params
data: { ...body } → CLI --data
yes: { boolean, default:false } → CLI --yes (only when risk == high-risk-write)
`yes` description strengthened to mark it as a CLI confirmation gate
(consumed by lark-cli, not sent to the backend), so AI can still
distinguish it from API fields without needing a wrapper.
Lint L3 yes-presence check goes back to top-level Properties.Map["yes"].
Refresh 20 goldens.
* feat(schema): add `file` top-level sub-object for binary upload fields
Splits file fields out of `data` into their own sibling, so the four
top-level slots in inputSchema map 1:1 to CLI flag dispatch:
inputSchema.properties:
params { path + query fields } → --params JSON
data { non-file body fields } → --data JSON
file { type:file body fields, format:binary } → --file <key>=<path>
yes boolean → --yes (only when risk == high-risk-write)
Each slot is conditional: only registered when the method actually has
fields for that source. This matches the CLI's own conditional flag
registration (cmd/service/service.go:170-195), so what AI sees in the
schema is exactly what flags exist for that method.
The file sub-object carries a description explaining its semantics so AI
knows to use --file for those fields rather than embedding the binary
in --data JSON.
Refresh im.images.create golden (the only file-upload method in the
golden set).
* test(schema): cover L2 lint recursion into params/data sub-objects
Add two negative test cases that stuff bad values inside the wrapped
inputSchema sub-objects (rather than at top-level), to lock in
walkForL2's recursive coverage:
- format:binary on a non-string field nested under params
- sub-object Required referencing a key not in its Properties
Regression guard so future walkForL2 refactors do not silently lose
recursion and let leaf-field violations slip past lint.
* fix(schema): coerce example, aggregate nested required, fix path hint
- coerce `example` literal to the declared JSON Schema type (rename
coerceEnumValue -> coerceLiteral, drop on coerce failure to match the
`default` policy). Without this, integer/boolean/number fields emitted
string examples and failed strict validators.
- aggregate child field `required:true` into the enclosing nested
object's `required[]` (both object and array-items shapes). Previously
only the top-level params/data sub-objects scanned `required`, so
envelopes silently under-reported the real call contract.
- check method existence before reporting trailing-segment failure in
both JSON and pretty `schema` paths. A typo like `schema im messages
typo extra` now reports "Unknown method: im.messages.typo" instead of
the misleading "Method 'typo' exists but trailing segments ..." hint.
- extract risk level constants (RiskRead / RiskWrite / RiskHighRiskWrite)
in internal/cmdutil/risk.go; replace literal usages in schema, lint,
and confirm helpers so the typo radius is one file.
- reconcile AssembleEnvelope docstring with implementation reality (the
package-level currentMethodOrder + assembleMu serialize concurrent
callers; output is deterministic per inputs).
- drop testdata/golden/ and golden_test harness. End-to-end envelope
shape regression now relies on real CLI invocations and the existing
property-level unit + lint coverage.
* fix(schema): emit items:{} for all typeless arrays, restore lint gate
The list→array fallback only added items:{} when the source type was
"list", leaving ~64 natively-typed array fields (e.g.
approval.instances.cc.cc_user_ids) as {type:"array"} with no items.
These violated the L1 lint rule, but TestAllEnvelopesPass skipped the
"array missing items" error as a known data inconsistency, so the MCP
tool contract was not actually lint-clean.
Relax the fallback to cover every array lacking element shape regardless
of source type, and drop the lint-test skip so the gate is hard again.
813 lines
27 KiB
Go
813 lines
27 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package schema
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/registry"
|
|
"github.com/larksuite/cli/internal/schema"
|
|
"github.com/larksuite/cli/internal/util"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// SchemaOptions holds all inputs for the schema command.
|
|
type SchemaOptions struct {
|
|
Factory *cmdutil.Factory
|
|
Ctx context.Context
|
|
|
|
// Positional args
|
|
Path string // first positional, when only one is given
|
|
ExtraArgs []string // 2nd+ positional args (space-separated form)
|
|
|
|
// Flags
|
|
Format string
|
|
}
|
|
|
|
func printServices(w io.Writer) {
|
|
services := registry.ListFromMetaProjects()
|
|
fmt.Fprintf(w, "%sAvailable services:%s\n\n", output.Bold, output.Reset)
|
|
for _, s := range services {
|
|
spec := registry.LoadFromMeta(s)
|
|
title := registry.GetStrFromMap(spec, "title")
|
|
if title == "" {
|
|
title = registry.GetStrFromMap(spec, "description")
|
|
}
|
|
fmt.Fprintf(w, " %s%s%s %s%s%s\n", output.Cyan, s, output.Reset, output.Dim, title, output.Reset)
|
|
}
|
|
fmt.Fprintf(w, "\n%sUsage: lark-cli schema <service>.<resource>.<method>%s\n", output.Dim, output.Reset)
|
|
}
|
|
|
|
func printResourceList(w io.Writer, spec map[string]interface{}, mode core.StrictMode) {
|
|
name := registry.GetStrFromMap(spec, "name")
|
|
version := registry.GetStrFromMap(spec, "version")
|
|
title := registry.GetStrFromMap(spec, "title")
|
|
if title == "" {
|
|
title = registry.GetStrFromMap(spec, "description")
|
|
}
|
|
servicePath := registry.GetStrFromMap(spec, "servicePath")
|
|
|
|
fmt.Fprintf(w, "%s%s%s (%s) — %s\n\n", output.Bold, name, output.Reset, version, title)
|
|
fmt.Fprintf(w, "%sBase path: %s%s\n\n", output.Dim, servicePath, output.Reset)
|
|
|
|
resources, _ := spec["resources"].(map[string]interface{})
|
|
for _, resName := range sortedKeys(resources) {
|
|
resMap, _ := resources[resName].(map[string]interface{})
|
|
methods, _ := resMap["methods"].(map[string]interface{})
|
|
methods = filterMethodsByStrictMode(methods, mode)
|
|
if len(methods) == 0 {
|
|
continue
|
|
}
|
|
fmt.Fprintf(w, " %s%s%s\n", output.Cyan, resName, output.Reset)
|
|
for _, methodName := range sortedKeys(methods) {
|
|
m, _ := methods[methodName].(map[string]interface{})
|
|
httpMethod := registry.GetStrFromMap(m, "httpMethod")
|
|
desc := registry.GetStrFromMap(m, "description")
|
|
danger := ""
|
|
if d, _ := m["danger"].(bool); d {
|
|
danger = fmt.Sprintf(" %s[danger]%s", output.Red, output.Reset)
|
|
}
|
|
fmt.Fprintf(w, " %-7s %s%s%s %s%s%s%s\n", httpMethod, output.Bold, methodName, output.Reset, output.Dim, desc, output.Reset, danger)
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
fmt.Fprintf(w, "%sUsage: lark-cli schema %s.<resource>.<method>%s\n", output.Dim, name, output.Reset)
|
|
}
|
|
|
|
// hasFileFields returns true if any requestBody field has type "file".
|
|
func hasFileFields(method map[string]interface{}) (bool, []string) {
|
|
names := cmdutil.DetectFileFields(method)
|
|
return len(names) > 0, names
|
|
}
|
|
|
|
func printMethodDetail(w io.Writer, spec map[string]interface{}, resName, methodName string, method map[string]interface{}) {
|
|
servicePath := registry.GetStrFromMap(spec, "servicePath")
|
|
specName := registry.GetStrFromMap(spec, "name")
|
|
methodPath := registry.GetStrFromMap(method, "path")
|
|
fullPath := servicePath + "/" + methodPath
|
|
httpMethod := registry.GetStrFromMap(method, "httpMethod")
|
|
desc := registry.GetStrFromMap(method, "description")
|
|
isFileUpload, fileFieldNames := hasFileFields(method)
|
|
|
|
fmt.Fprintf(w, "%s%s.%s.%s%s\n\n", output.Bold, specName, resName, methodName, output.Reset)
|
|
|
|
httpColor := output.Yellow
|
|
if httpMethod == "GET" {
|
|
httpColor = output.Green
|
|
} else if httpMethod == "DELETE" {
|
|
httpColor = output.Red
|
|
}
|
|
fmt.Fprintf(w, " %s%s%s %s\n", httpColor, httpMethod, output.Reset, fullPath)
|
|
if desc != "" {
|
|
fmt.Fprintf(w, " %s\n", desc)
|
|
}
|
|
fmt.Fprintln(w)
|
|
|
|
// Parameters
|
|
params, _ := method["parameters"].(map[string]interface{})
|
|
if len(params) > 0 {
|
|
fmt.Fprintf(w, "%sParameters:%s\n\n", output.Bold, output.Reset)
|
|
fmt.Fprintf(w, " %s--params%s <json> %soptional%s\n", output.Cyan, output.Reset, output.Dim, output.Reset)
|
|
for _, paramName := range sortedParamKeys(params) {
|
|
p, _ := params[paramName].(map[string]interface{})
|
|
pType := registry.GetStrFromMap(p, "type")
|
|
if pType == "" {
|
|
pType = "string"
|
|
}
|
|
location := registry.GetStrFromMap(p, "location")
|
|
required, _ := p["required"].(bool)
|
|
reqStr := fmt.Sprintf("%soptional%s", output.Dim, output.Reset)
|
|
if required {
|
|
reqStr = fmt.Sprintf("%srequired%s", output.Red, output.Reset)
|
|
}
|
|
locColor := output.Dim
|
|
if location == "path" {
|
|
locColor = output.Yellow
|
|
}
|
|
// Options (enum values)
|
|
optStr := formatOptions(p)
|
|
fmt.Fprintf(w, " - %s%s%s (%s, %s%s%s, %s)%s\n", output.Cyan, paramName, output.Reset, pType, locColor, location, output.Reset, reqStr, optStr)
|
|
if pdesc := registry.GetStrFromMap(p, "description"); pdesc != "" {
|
|
pdesc = util.TruncateStrWithEllipsis(pdesc, 100)
|
|
fmt.Fprintf(w, " %s%s%s\n", output.Dim, pdesc, output.Reset)
|
|
}
|
|
if ex := registry.GetStrFromMap(p, "example"); ex != "" {
|
|
fmt.Fprintf(w, " %se.g. %s%s\n", output.Dim, ex, output.Reset)
|
|
}
|
|
if rangeStr := formatRange(p); rangeStr != "" {
|
|
fmt.Fprintf(w, " %srange: %s%s\n", output.Dim, rangeStr, output.Reset)
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
|
|
// --data for write methods
|
|
if httpMethod == "POST" || httpMethod == "PUT" || httpMethod == "PATCH" || httpMethod == "DELETE" {
|
|
if len(params) == 0 {
|
|
fmt.Fprintf(w, "%sParameters:%s\n\n", output.Bold, output.Reset)
|
|
}
|
|
fileUploadTag := ""
|
|
if isFileUpload {
|
|
fileUploadTag = fmt.Sprintf(" %s[file upload]%s", output.Yellow, output.Reset)
|
|
}
|
|
fmt.Fprintf(w, " %s--data%s <json> %soptional%s%s\n", output.Cyan, output.Reset, output.Dim, output.Reset, fileUploadTag)
|
|
requestBody, _ := method["requestBody"].(map[string]interface{})
|
|
if len(requestBody) > 0 {
|
|
printNestedFields(w, requestBody, " ", "")
|
|
}
|
|
|
|
if isFileUpload {
|
|
if len(fileFieldNames) == 1 {
|
|
fmt.Fprintf(w, "\n %s--file%s <[field=]path> %sfile upload%s\n", output.Cyan, output.Reset, output.Dim, output.Reset)
|
|
fmt.Fprintf(w, " Upload file as multipart/form-data. Default field: %q\n", fileFieldNames[0])
|
|
} else {
|
|
fmt.Fprintf(w, "\n %s--file%s <field=path> %sfile upload%s\n", output.Cyan, output.Reset, output.Dim, output.Reset)
|
|
fmt.Fprintf(w, " Upload file as multipart/form-data. Fields: %s\n", strings.Join(fileFieldNames, ", "))
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
|
|
// Response
|
|
responseBody, _ := method["responseBody"].(map[string]interface{})
|
|
if len(responseBody) > 0 {
|
|
fmt.Fprintf(w, "%sResponse:%s\n\n", output.Bold, output.Reset)
|
|
printNestedFields(w, responseBody, " ", "")
|
|
fmt.Fprintln(w)
|
|
}
|
|
|
|
// Identity
|
|
if tokens, ok := method["accessTokens"].([]interface{}); ok && len(tokens) > 0 {
|
|
var identities []string
|
|
for _, t := range tokens {
|
|
if s, ok := t.(string); ok {
|
|
switch s {
|
|
case "user":
|
|
identities = append(identities, "user")
|
|
case "tenant":
|
|
identities = append(identities, "bot")
|
|
}
|
|
}
|
|
}
|
|
if len(identities) > 0 {
|
|
fmt.Fprintf(w, "%sIdentity:%s %s\n", output.Bold, output.Reset, strings.Join(identities, ", "))
|
|
}
|
|
}
|
|
|
|
// Scopes (all)
|
|
if scopes, ok := method["scopes"].([]interface{}); ok && len(scopes) > 0 {
|
|
var scopeStrs []string
|
|
for _, s := range scopes {
|
|
if str, ok := s.(string); ok {
|
|
scopeStrs = append(scopeStrs, str)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "%sScopes:%s %s\n", output.Bold, output.Reset, strings.Join(scopeStrs, ", "))
|
|
}
|
|
|
|
// CLI example
|
|
if isFileUpload && len(fileFieldNames) == 1 {
|
|
fmt.Fprintf(w, "%sCLI:%s lark-cli %s %s %s --file <path>\n", output.Bold, output.Reset, specName, resName, methodName)
|
|
} else if isFileUpload {
|
|
fmt.Fprintf(w, "%sCLI:%s lark-cli %s %s %s --file <field=path>\n", output.Bold, output.Reset, specName, resName, methodName)
|
|
} else {
|
|
fmt.Fprintf(w, "%sCLI:%s lark-cli %s %s %s\n", output.Bold, output.Reset, specName, resName, methodName)
|
|
}
|
|
|
|
// Docs
|
|
if docUrl := registry.GetStrFromMap(method, "docUrl"); docUrl != "" {
|
|
fmt.Fprintf(w, "%sDocs:%s %s\n", output.Bold, output.Reset, docUrl)
|
|
}
|
|
}
|
|
|
|
func printNestedFields(w io.Writer, fields map[string]interface{}, indent, prefix string) {
|
|
for _, fieldName := range sortedFieldKeys(fields) {
|
|
f, _ := fields[fieldName].(map[string]interface{})
|
|
fullName := fieldName
|
|
if prefix != "" {
|
|
fullName = prefix + "." + fieldName
|
|
}
|
|
fType := registry.GetStrFromMap(f, "type")
|
|
required, _ := f["required"].(bool)
|
|
reqStr := fmt.Sprintf("%soptional%s", output.Dim, output.Reset)
|
|
if required {
|
|
reqStr = fmt.Sprintf("%srequired%s", output.Red, output.Reset)
|
|
}
|
|
optStr := formatOptions(f)
|
|
fmt.Fprintf(w, "%s- %s%s%s (%s, %s)%s\n", indent, output.Cyan, fullName, output.Reset, fType, reqStr, optStr)
|
|
desc := registry.GetStrFromMap(f, "description")
|
|
if desc != "" {
|
|
desc = util.TruncateStrWithEllipsis(desc, 100)
|
|
fmt.Fprintf(w, "%s %s%s%s\n", indent, output.Dim, desc, output.Reset)
|
|
}
|
|
if ex := registry.GetStrFromMap(f, "example"); ex != "" {
|
|
fmt.Fprintf(w, "%s %se.g. %s%s\n", indent, output.Dim, ex, output.Reset)
|
|
}
|
|
if rangeStr := formatRange(f); rangeStr != "" {
|
|
fmt.Fprintf(w, "%s %srange: %s%s\n", indent, output.Dim, rangeStr, output.Reset)
|
|
}
|
|
if props, ok := f["properties"].(map[string]interface{}); ok && len(props) > 0 {
|
|
printNestedFields(w, props, indent+" ", fullName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// formatOptions returns " — val1 | val2 | ..." if field has options, else "".
|
|
func formatOptions(f map[string]interface{}) string {
|
|
opts, ok := f["options"].([]interface{})
|
|
if !ok || len(opts) == 0 {
|
|
return ""
|
|
}
|
|
var vals []string
|
|
for _, o := range opts {
|
|
if om, ok := o.(map[string]interface{}); ok {
|
|
if v := registry.GetStrFromMap(om, "value"); v != "" {
|
|
vals = append(vals, v)
|
|
}
|
|
}
|
|
}
|
|
if len(vals) == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" %s— %s%s", output.Dim, strings.Join(vals, " | "), output.Reset)
|
|
}
|
|
|
|
// formatRange returns "min..max" if field has min/max, else "".
|
|
func formatRange(f map[string]interface{}) string {
|
|
minVal := registry.GetStrFromMap(f, "min")
|
|
maxVal := registry.GetStrFromMap(f, "max")
|
|
if minVal == "" && maxVal == "" {
|
|
return ""
|
|
}
|
|
if minVal != "" && maxVal != "" {
|
|
return minVal + ".." + maxVal
|
|
}
|
|
if minVal != "" {
|
|
return ">=" + minVal
|
|
}
|
|
return "<=" + maxVal
|
|
}
|
|
|
|
// sortedKeys returns map keys in alphabetical order.
|
|
func sortedKeys(m map[string]interface{}) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// sortedParamKeys returns parameter keys sorted: required first, then alphabetical.
|
|
func sortedParamKeys(params map[string]interface{}) []string {
|
|
keys := make([]string, 0, len(params))
|
|
for k := range params {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
pi, _ := params[keys[i]].(map[string]interface{})
|
|
pj, _ := params[keys[j]].(map[string]interface{})
|
|
ri, _ := pi["required"].(bool)
|
|
rj, _ := pj["required"].(bool)
|
|
if ri != rj {
|
|
return ri
|
|
}
|
|
return keys[i] < keys[j]
|
|
})
|
|
return keys
|
|
}
|
|
|
|
// sortedFieldKeys returns field keys sorted: required first, then alphabetical.
|
|
func sortedFieldKeys(fields map[string]interface{}) []string {
|
|
keys := make([]string, 0, len(fields))
|
|
for k := range fields {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
fi, _ := fields[keys[i]].(map[string]interface{})
|
|
fj, _ := fields[keys[j]].(map[string]interface{})
|
|
ri, _ := fi["required"].(bool)
|
|
rj, _ := fj["required"].(bool)
|
|
if ri != rj {
|
|
return ri
|
|
}
|
|
return keys[i] < keys[j]
|
|
})
|
|
return keys
|
|
}
|
|
|
|
func findResourceByPath(resources map[string]interface{}, parts []string) (map[string]interface{}, string, []string) {
|
|
for i := len(parts); i >= 1; i-- {
|
|
candidateName := strings.Join(parts[:i], ".")
|
|
if res, ok := resources[candidateName]; ok {
|
|
if resMap, ok := res.(map[string]interface{}); ok {
|
|
return resMap, candidateName, parts[i:]
|
|
}
|
|
}
|
|
}
|
|
return nil, "", nil
|
|
}
|
|
|
|
// NewCmdSchema creates the schema command. If runF is non-nil it is called instead of schemaRun (test hook).
|
|
func NewCmdSchema(f *cmdutil.Factory, runF func(*SchemaOptions) error) *cobra.Command {
|
|
opts := &SchemaOptions{Factory: f}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "schema [path | service resource method]",
|
|
Short: "View API method parameters, types, and scopes",
|
|
Args: cobra.MaximumNArgs(8),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) > 0 {
|
|
opts.Path = args[0]
|
|
}
|
|
if len(args) > 1 {
|
|
opts.ExtraArgs = args[1:]
|
|
}
|
|
opts.Ctx = cmd.Context()
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return schemaRun(opts)
|
|
},
|
|
}
|
|
cmdutil.DisableAuthCheck(cmd)
|
|
|
|
cmd.ValidArgsFunction = completeSchemaPath(f)
|
|
cmd.Flags().StringVar(&opts.Format, "format", "json", "output format: json (default) | pretty")
|
|
cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
return []string{"json", "pretty"}, cobra.ShellCompDirectiveNoFileComp
|
|
})
|
|
cmdutil.SetRisk(cmd, cmdutil.RiskRead)
|
|
|
|
return cmd
|
|
}
|
|
|
|
// completeSchemaPath provides tab-completion for the schema path argument.
|
|
// It handles both legacy dotted resource names (e.g. app.table.fields) and the
|
|
// newer space-separated form (e.g. `schema im messages reply`).
|
|
func completeSchemaPath(f *cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
mode := f.ResolveStrictMode(cmd.Context())
|
|
|
|
// Case 1: legacy "single dotted arg" path — no previous args yet
|
|
if len(args) == 0 {
|
|
parts := strings.Split(toComplete, ".")
|
|
if len(parts) <= 1 {
|
|
var completions []string
|
|
for _, s := range registry.ListFromMetaProjects() {
|
|
if strings.HasPrefix(s, toComplete) {
|
|
completions = append(completions, s+".")
|
|
}
|
|
}
|
|
return completions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
|
|
}
|
|
serviceName := parts[0]
|
|
spec := registry.LoadFromMeta(serviceName)
|
|
if spec == nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
spec = filterSpecByStrictMode(spec, mode)
|
|
resources, _ := spec["resources"].(map[string]interface{})
|
|
if resources == nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
afterService := strings.Join(parts[1:], ".")
|
|
completions := completeSchemaPathForSpec(serviceName, resources, afterService)
|
|
allTrailingDot := len(completions) > 0
|
|
for _, c := range completions {
|
|
if !strings.HasSuffix(c, ".") {
|
|
allTrailingDot = false
|
|
break
|
|
}
|
|
}
|
|
directive := cobra.ShellCompDirectiveNoFileComp
|
|
if allTrailingDot {
|
|
directive |= cobra.ShellCompDirectiveNoSpace
|
|
}
|
|
return completions, directive
|
|
}
|
|
|
|
// Case 2: space-form, args already has segments
|
|
// Walk down service -> resource(s) -> method based on existing args
|
|
serviceName := args[0]
|
|
spec := registry.LoadFromMeta(serviceName)
|
|
if spec == nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
spec = filterSpecByStrictMode(spec, mode)
|
|
resources, _ := spec["resources"].(map[string]interface{})
|
|
if resources == nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// args[1:] are resource path segments (possibly partial); current
|
|
// toComplete is the next segment under cursor.
|
|
consumed := args[1:]
|
|
resource, _, remaining := findResourceByPath(resources, consumed)
|
|
if resource == nil {
|
|
// Suggest top-level resource names that match toComplete
|
|
var completions []string
|
|
for resName := range resources {
|
|
if strings.HasPrefix(resName, toComplete) {
|
|
completions = append(completions, resName)
|
|
}
|
|
}
|
|
sort.Strings(completions)
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
if len(remaining) > 0 {
|
|
// Already typed past the resource — suggest methods
|
|
methods, _ := resource["methods"].(map[string]interface{})
|
|
methods = filterMethodsByStrictMode(methods, mode)
|
|
var completions []string
|
|
for mName := range methods {
|
|
if strings.HasPrefix(mName, toComplete) {
|
|
completions = append(completions, mName)
|
|
}
|
|
}
|
|
sort.Strings(completions)
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
// Resource matched exactly, suggest methods
|
|
methods, _ := resource["methods"].(map[string]interface{})
|
|
methods = filterMethodsByStrictMode(methods, mode)
|
|
var completions []string
|
|
for mName := range methods {
|
|
if strings.HasPrefix(mName, toComplete) {
|
|
completions = append(completions, mName)
|
|
}
|
|
}
|
|
sort.Strings(completions)
|
|
return completions, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
}
|
|
|
|
func completeSchemaPathForSpec(serviceName string, resources map[string]interface{}, afterService string) []string {
|
|
var completions []string
|
|
|
|
for resName, resVal := range resources {
|
|
if strings.HasPrefix(resName, afterService) {
|
|
completions = append(completions, serviceName+"."+resName+".")
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(afterService, resName+".") {
|
|
continue
|
|
}
|
|
methodPrefix := afterService[len(resName)+1:]
|
|
resMap, _ := resVal.(map[string]interface{})
|
|
if resMap == nil {
|
|
continue
|
|
}
|
|
methods, _ := resMap["methods"].(map[string]interface{})
|
|
for methodName := range methods {
|
|
if strings.HasPrefix(methodName, methodPrefix) {
|
|
completions = append(completions, serviceName+"."+resName+"."+methodName)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Strings(completions)
|
|
return completions
|
|
}
|
|
|
|
func schemaRun(opts *SchemaOptions) error {
|
|
out := opts.Factory.IOStreams.Out
|
|
mode := opts.Factory.ResolveStrictMode(opts.Ctx)
|
|
|
|
// args may have arrived as a single string (legacy single-arg path) or
|
|
// split into multiple — normalize to a single args slice.
|
|
var rawArgs []string
|
|
if opts.Path != "" {
|
|
rawArgs = []string{opts.Path}
|
|
}
|
|
if len(opts.ExtraArgs) > 0 {
|
|
if opts.Path != "" {
|
|
rawArgs = append([]string{opts.Path}, opts.ExtraArgs...)
|
|
} else {
|
|
rawArgs = append([]string(nil), opts.ExtraArgs...)
|
|
}
|
|
}
|
|
parts := schema.ParsePath(rawArgs)
|
|
|
|
if opts.Format == "pretty" {
|
|
return runPrettyMode(out, parts, mode)
|
|
}
|
|
return runJSONMode(out, parts, mode)
|
|
}
|
|
|
|
// runJSONMode dispatches list/single envelope output based on parts.
|
|
// JSON mode uses embedded data only (bypasses remote overlay) so envelope
|
|
// output is deterministic across machines.
|
|
func runJSONMode(out io.Writer, parts []string, mode core.StrictMode) error {
|
|
filter := strictModeFilter(mode)
|
|
|
|
switch len(parts) {
|
|
case 0:
|
|
envs := schema.AssembleAll(filter)
|
|
output.PrintJson(out, envs)
|
|
return nil
|
|
case 1:
|
|
spec := registry.EmbeddedSpec(parts[0])
|
|
if spec == nil {
|
|
return errUnknownEmbeddedService(parts[0])
|
|
}
|
|
envs := schema.AssembleService(parts[0], spec, filter)
|
|
output.PrintJson(out, envs)
|
|
return nil
|
|
default:
|
|
return runJSONForPath(out, parts, filter)
|
|
}
|
|
}
|
|
|
|
// runJSONForPath handles len(parts) >= 2: try resource match first, fallback
|
|
// to single-method match. Uses embedded data only.
|
|
func runJSONForPath(out io.Writer, parts []string, filter schema.MethodFilter) error {
|
|
serviceName := parts[0]
|
|
spec := registry.EmbeddedSpec(serviceName)
|
|
if spec == nil {
|
|
return errUnknownEmbeddedService(serviceName)
|
|
}
|
|
resources, _ := spec["resources"].(map[string]interface{})
|
|
resource, resName, remaining := findResourceByPath(resources, parts[1:])
|
|
if resource == nil {
|
|
var names []string
|
|
for k := range resources {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown resource: %s.%s", serviceName, strings.Join(parts[1:], ".")),
|
|
fmt.Sprintf("Available: %s", strings.Join(names, ", ")))
|
|
}
|
|
if len(remaining) == 0 {
|
|
// Resource-scoped envelope array
|
|
envs := assembleResource(serviceName, resName, resource, filter)
|
|
output.PrintJson(out, envs)
|
|
return nil
|
|
}
|
|
methodName := remaining[0]
|
|
methods, _ := resource["methods"].(map[string]interface{})
|
|
method, ok := methods[methodName].(map[string]interface{})
|
|
if !ok {
|
|
var names []string
|
|
for k := range methods {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown method: %s.%s.%s", serviceName, resName, methodName),
|
|
fmt.Sprintf("Available: %s", strings.Join(names, ", ")))
|
|
}
|
|
if len(remaining) > 1 {
|
|
// Method exists but caller appended extra segments — reject so they
|
|
// don't silently get this method's schema when they typo'd the path.
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown path: %s.%s.%s",
|
|
serviceName, resName, strings.Join(remaining, ".")),
|
|
fmt.Sprintf("Method %q exists but the trailing segments %q do not resolve",
|
|
methodName, strings.Join(remaining[1:], ".")))
|
|
}
|
|
if filter != nil && !filter(method) {
|
|
// Method exists in spec but filtered out by strict mode
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Method %s.%s.%s not available in current identity mode", serviceName, resName, methodName),
|
|
"Use --as user / --as bot to switch")
|
|
}
|
|
env := schema.AssembleEnvelope(serviceName, []string{resName}, methodName, method)
|
|
output.PrintJson(out, env)
|
|
return nil
|
|
}
|
|
|
|
func assembleResource(serviceName, resName string, resource map[string]interface{}, filter schema.MethodFilter) []schema.Envelope {
|
|
methods, _ := resource["methods"].(map[string]interface{})
|
|
resourcePath := []string{resName}
|
|
var envs []schema.Envelope
|
|
for methodName, raw := range methods {
|
|
method, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if filter != nil && !filter(method) {
|
|
continue
|
|
}
|
|
envs = append(envs, schema.AssembleEnvelope(serviceName, resourcePath, methodName, method))
|
|
}
|
|
sort.Slice(envs, func(i, j int) bool { return envs[i].Name < envs[j].Name })
|
|
return envs
|
|
}
|
|
|
|
// runPrettyMode preserves the existing legacy pretty rendering verbatim.
|
|
// All printServices/printResourceList/printMethodDetail calls stay unchanged.
|
|
func runPrettyMode(out io.Writer, parts []string, mode core.StrictMode) error {
|
|
if len(parts) == 0 {
|
|
printServices(out)
|
|
return nil
|
|
}
|
|
serviceName := parts[0]
|
|
spec := registry.LoadFromMeta(serviceName)
|
|
if spec == nil {
|
|
return errUnknownService(serviceName)
|
|
}
|
|
if len(parts) == 1 {
|
|
printResourceList(out, spec, mode)
|
|
return nil
|
|
}
|
|
resources, _ := spec["resources"].(map[string]interface{})
|
|
resource, resName, remaining := findResourceByPath(resources, parts[1:])
|
|
if resource == nil {
|
|
var names []string
|
|
for k := range resources {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown resource: %s.%s", serviceName, strings.Join(parts[1:], ".")),
|
|
fmt.Sprintf("Available: %s", strings.Join(names, ", ")))
|
|
}
|
|
if len(remaining) == 0 {
|
|
fmt.Fprintf(out, "%s%s.%s%s\n\n", output.Bold, serviceName, resName, output.Reset)
|
|
methods, _ := resource["methods"].(map[string]interface{})
|
|
methods = filterMethodsByStrictMode(methods, mode)
|
|
for _, mName := range sortedKeys(methods) {
|
|
m, _ := methods[mName].(map[string]interface{})
|
|
httpMethod := registry.GetStrFromMap(m, "httpMethod")
|
|
desc := registry.GetStrFromMap(m, "description")
|
|
fmt.Fprintf(out, " %-7s %s%s%s %s%s%s\n", httpMethod, output.Bold, mName, output.Reset, output.Dim, desc, output.Reset)
|
|
}
|
|
fmt.Fprintf(out, "\n%sUsage: lark-cli schema %s.%s.<method>%s\n", output.Dim, serviceName, resName, output.Reset)
|
|
return nil
|
|
}
|
|
methodName := remaining[0]
|
|
methods, _ := resource["methods"].(map[string]interface{})
|
|
methods = filterMethodsByStrictMode(methods, mode)
|
|
method, ok := methods[methodName].(map[string]interface{})
|
|
if !ok {
|
|
var names []string
|
|
for k := range methods {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown method: %s.%s.%s", serviceName, resName, methodName),
|
|
fmt.Sprintf("Available: %s", strings.Join(names, ", ")))
|
|
}
|
|
if len(remaining) > 1 {
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown path: %s.%s.%s",
|
|
serviceName, resName, strings.Join(remaining, ".")),
|
|
fmt.Sprintf("Method %q exists but the trailing segments %q do not resolve",
|
|
methodName, strings.Join(remaining[1:], ".")))
|
|
}
|
|
printMethodDetail(out, spec, resName, methodName, method)
|
|
return nil
|
|
}
|
|
|
|
// strictModeFilter adapts core.StrictMode into a schema.MethodFilter, or returns
|
|
// nil if strict mode is not active.
|
|
func strictModeFilter(mode core.StrictMode) schema.MethodFilter {
|
|
if !mode.IsActive() {
|
|
return nil
|
|
}
|
|
token := registry.IdentityToAccessToken(string(mode.ForcedIdentity()))
|
|
return func(method map[string]interface{}) bool {
|
|
tokens, _ := method["accessTokens"].([]interface{})
|
|
if tokens == nil {
|
|
return true // permissive when meta_data lacks accessTokens
|
|
}
|
|
for _, t := range tokens {
|
|
if s, _ := t.(string); s == token {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func errUnknownService(name string) error {
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown service: %s", name),
|
|
fmt.Sprintf("Available: %s", strings.Join(registry.ListFromMetaProjects(), ", ")))
|
|
}
|
|
|
|
// errUnknownEmbeddedService is the JSON-mode variant: it lists only embedded
|
|
// services (no overlay) because JSON mode itself bypasses overlay; suggesting
|
|
// overlay-only services would mislead callers when those services subsequently
|
|
// fail to resolve in envelope output.
|
|
func errUnknownEmbeddedService(name string) error {
|
|
return output.ErrWithHint(output.ExitValidation, "validation",
|
|
fmt.Sprintf("Unknown service: %s", name),
|
|
fmt.Sprintf("Available: %s", strings.Join(registry.EmbeddedServiceNames(), ", ")))
|
|
}
|
|
|
|
// filterSpecByStrictMode returns a shallow copy of spec with each resource's methods
|
|
// filtered by strict mode. Returns the original spec when strict mode is off.
|
|
func filterSpecByStrictMode(spec map[string]interface{}, mode core.StrictMode) map[string]interface{} {
|
|
if !mode.IsActive() {
|
|
return spec
|
|
}
|
|
result := make(map[string]interface{}, len(spec))
|
|
for k, v := range spec {
|
|
result[k] = v
|
|
}
|
|
resources, _ := spec["resources"].(map[string]interface{})
|
|
if resources == nil {
|
|
return result
|
|
}
|
|
filteredRes := make(map[string]interface{}, len(resources))
|
|
for resName, resVal := range resources {
|
|
resMap, ok := resVal.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
methods, _ := resMap["methods"].(map[string]interface{})
|
|
filtered := filterMethodsByStrictMode(methods, mode)
|
|
if len(filtered) == 0 {
|
|
continue
|
|
}
|
|
resCopy := make(map[string]interface{}, len(resMap))
|
|
for k, v := range resMap {
|
|
resCopy[k] = v
|
|
}
|
|
resCopy["methods"] = filtered
|
|
filteredRes[resName] = resCopy
|
|
}
|
|
result["resources"] = filteredRes
|
|
return result
|
|
}
|
|
|
|
// filterMethodsByStrictMode removes methods incompatible with the active strict mode.
|
|
// Returns the original map unmodified when strict mode is off.
|
|
func filterMethodsByStrictMode(methods map[string]interface{}, mode core.StrictMode) map[string]interface{} {
|
|
if !mode.IsActive() || methods == nil {
|
|
return methods
|
|
}
|
|
token := registry.IdentityToAccessToken(string(mode.ForcedIdentity()))
|
|
filtered := make(map[string]interface{}, len(methods))
|
|
for name, val := range methods {
|
|
m, ok := val.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
tokens, _ := m["accessTokens"].([]interface{})
|
|
if tokens == nil {
|
|
filtered[name] = val
|
|
continue
|
|
}
|
|
for _, t := range tokens {
|
|
if ts, ok := t.(string); ok && ts == token {
|
|
filtered[name] = val
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return filtered
|
|
}
|