chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/meta"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type boundParamFlag struct {
|
||||
field meta.Field
|
||||
read func() interface{}
|
||||
}
|
||||
|
||||
// paramsOnlyField is a path/query parameter that got no typed flag because its
|
||||
// kebab name is already taken by another flag (a standard flag like --format, or
|
||||
// a root persistent flag). It stays reachable via --params; the binder keeps it,
|
||||
// with the flag that claimed the name, so --help can show the exact --params form
|
||||
// and steer the reader off the wrong flag.
|
||||
type paramsOnlyField struct {
|
||||
field meta.Field
|
||||
claimed *pflag.Flag
|
||||
}
|
||||
|
||||
// paramFlagBinder owns one service method's generated typed param flags: it
|
||||
// registers them (kind, help, enum completion, reserved-name skip) and applies
|
||||
// the --params overlay, where a changed typed flag overrides its key in the
|
||||
// --params JSON. Holding the field<->flag binding here keeps the request builder
|
||||
// from re-deriving which flags map to which param keys.
|
||||
type paramFlagBinder struct {
|
||||
bound []boundParamFlag
|
||||
paramsOnly []paramsOnlyField
|
||||
}
|
||||
|
||||
// newParamFlagBinder registers one typed kebab flag per path/query parameter on
|
||||
// cmd and returns a binder for the --params overlay. A name already taken by
|
||||
// another flag is skipped — pflag panics on a local duplicate and a generated
|
||||
// flag would silently shadow a persistent one — and recorded as paramsOnly so
|
||||
// the parameter stays reachable (and discoverable) via --params. The taken set
|
||||
// is derived, not hand-listed: local flags (the standard set, registered before
|
||||
// this runs) via cmd, the lazily-added --help materialized here, and the root's
|
||||
// persistent flags via reserved (nil for direct callers that have no root).
|
||||
func newParamFlagBinder(cmd *cobra.Command, params []meta.Field, reserved *pflag.FlagSet) *paramFlagBinder {
|
||||
cmd.InitDefaultHelpFlag() // materialize --help/-h so the local guard below sees it
|
||||
b := ¶mFlagBinder{}
|
||||
for _, f := range params {
|
||||
name := f.FlagName()
|
||||
if claimed := flagClaiming(cmd, reserved, name); claimed != nil {
|
||||
b.paramsOnly = append(b.paramsOnly, paramsOnlyField{field: f, claimed: claimed})
|
||||
continue
|
||||
}
|
||||
read := registerTypedFlag(cmd.Flags(), name, f.CanonicalType(), paramFlagUsage(f))
|
||||
if values := enumStrings(f.EnumValues()); len(values) > 0 {
|
||||
cmdutil.RegisterFlagCompletion(cmd, name, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
// Group as an API parameter and mark required/optional for the
|
||||
// Required/Optional subsections of the grouped --help renderer.
|
||||
if fl := cmd.Flags().Lookup(name); fl != nil {
|
||||
annotate(fl, flagGroupAnnotation, []string{groupParams})
|
||||
sub := subOptional
|
||||
if f.Required {
|
||||
sub = subRequired
|
||||
}
|
||||
annotate(fl, flagSubAnnotation, []string{sub})
|
||||
}
|
||||
b.bound = append(b.bound, boundParamFlag{field: f, read: read})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// flagClaiming returns the flag already occupying name (so a typed param flag
|
||||
// would collide), or nil when the name is free. It checks the command's own
|
||||
// flags (the standard set + the materialized --help) and the root's persistent
|
||||
// flags — so the reserved set is whatever is actually registered, never a
|
||||
// hand-kept list that drifts when a global flag is added.
|
||||
func flagClaiming(cmd *cobra.Command, reserved *pflag.FlagSet, name string) *pflag.Flag {
|
||||
if fl := cmd.Flags().Lookup(name); fl != nil {
|
||||
return fl
|
||||
}
|
||||
if reserved != nil {
|
||||
return reserved.Lookup(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// paramsOnlyHelp renders the --help addendum for parameters that have no typed
|
||||
// flag, or "" when there are none. Per field: a copy-pasteable --params form,
|
||||
// the same fieldFacts a typed flag would show on its usage line, and what the
|
||||
// colliding flag actually does — so neither a human nor an agent sets the
|
||||
// wrong one (e.g. --format, which is the output format, not the API parameter).
|
||||
func (b *paramFlagBinder) paramsOnlyHelp() string {
|
||||
if len(b.paramsOnly) == 0 {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\nParameters set via --params (no typed flag; the name is taken by another flag):\n")
|
||||
for _, p := range b.paramsOnly {
|
||||
name := p.field.Name
|
||||
fmt.Fprintf(&sb, " %s: --params '{%q: %s}'\n", name, name, paramExample(p.field))
|
||||
for _, fact := range fieldFacts(p.field) {
|
||||
fmt.Fprintf(&sb, " %s\n", fact)
|
||||
}
|
||||
if p.claimed != nil {
|
||||
fmt.Fprintf(&sb, " do not use --%s (%s)\n", p.claimed.Name, p.claimed.Usage)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// hasTypedFlag reports whether the binder registered a typed flag for the
|
||||
// param named name. False for params-only fields — a flag with the same kebab
|
||||
// name may exist (that's the collision), but it is not this param's input.
|
||||
// Nil-safe for direct buildServiceRequest callers that have no binder.
|
||||
func (b *paramFlagBinder) hasTypedFlag(name string) bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
for _, pf := range b.bound {
|
||||
if pf.field.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// overlay lets an explicit typed flag override the same key in --params
|
||||
// (--params is the base). Only changed flags apply, so the --params-only path is
|
||||
// unchanged. A nil binder or cmd is a no-op.
|
||||
func (b *paramFlagBinder) overlay(cmd *cobra.Command, params map[string]interface{}) {
|
||||
if b == nil || cmd == nil {
|
||||
return
|
||||
}
|
||||
for _, pf := range b.bound {
|
||||
if cmd.Flags().Changed(pf.field.FlagName()) {
|
||||
params[pf.field.Name] = pf.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerTypedFlag registers one flag of the given canonical JSON-Schema kind
|
||||
// and returns a reader for its parsed value; the kind→pflag-type switch lives
|
||||
// only here.
|
||||
func registerTypedFlag(fs *pflag.FlagSet, name, kind, usage string) func() interface{} {
|
||||
switch kind {
|
||||
case "integer":
|
||||
return flagReader(fs.Int(name, 0, usage))
|
||||
case "boolean":
|
||||
return flagReader(fs.Bool(name, false, usage))
|
||||
case "array":
|
||||
return flagReader(fs.StringArray(name, nil, usage))
|
||||
default:
|
||||
return flagReader(fs.String(name, "", usage))
|
||||
}
|
||||
}
|
||||
|
||||
func flagReader[T any](p *T) func() interface{} {
|
||||
return func() interface{} { return *p }
|
||||
}
|
||||
Reference in New Issue
Block a user