Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 30 additions & 12 deletions shortcuts/base/base_shortcut_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@ package base

import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"

"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/internal/vfs"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/shortcuts/common"
)

// parseCtx carries file I/O dependency for JSON/file parsing helpers.
type parseCtx struct {
fio fileio.FileIO
}

func newParseCtx(runtime *common.RuntimeContext) *parseCtx {
return &parseCtx{fio: runtime.FileIO()}
}

func baseTableID(runtime *common.RuntimeContext) string {
return strings.TrimSpace(runtime.Str("table-id"))
}

func loadJSONInput(raw string, flagName string) (string, error) {
func loadJSONInput(pc *parseCtx, raw string, flagName string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", common.FlagErrorf("--%s cannot be empty", flagName)
Expand All @@ -29,11 +39,19 @@ func loadJSONInput(raw string, flagName string) (string, error) {
if path == "" {
return "", common.FlagErrorf("--%s file path cannot be empty after @", flagName)
}
safePath, err := validate.SafeInputPath(path)
if pc.fio == nil {
return "", common.FlagErrorf("--%s @file inputs require a FileIO provider", flagName)
}
f, err := pc.fio.Open(path)
if err != nil {
return "", common.FlagErrorf("--%s invalid JSON file path %q: %v", flagName, path, err)
var pathErr *fileio.PathValidationError
if errors.As(err, &pathErr) {
return "", common.FlagErrorf("--%s invalid JSON file path %q: %v", flagName, path, pathErr.Err)
}
return "", common.FlagErrorf("--%s cannot open JSON file %q: %v", flagName, path, err)
}
data, err := vfs.ReadFile(safePath)
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return "", common.FlagErrorf("--%s cannot read JSON file %q: %v", flagName, path, err)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Expand Down Expand Up @@ -86,18 +104,18 @@ func baseAction(runtime *common.RuntimeContext, boolFlags []string, stringFlags
return active[0], nil
}

func parseObjectList(raw string, flagName string) ([]map[string]interface{}, error) {
func parseObjectList(pc *parseCtx, raw string, flagName string) ([]map[string]interface{}, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
var err error
raw, err = loadJSONInput(raw, flagName)
raw, err = loadJSONInput(pc, raw, flagName)
if err != nil {
return nil, err
}
if strings.HasPrefix(raw, "[") {
arr, err := parseJSONArray(raw, flagName)
arr, err := parseJSONArray(pc, raw, flagName)
if err != nil {
return nil, err
}
Expand All @@ -111,16 +129,16 @@ func parseObjectList(raw string, flagName string) ([]map[string]interface{}, err
}
return items, nil
}
obj, err := parseJSONObject(raw, flagName)
obj, err := parseJSONObject(pc, raw, flagName)
if err != nil {
return nil, err
}
return []map[string]interface{}{obj}, nil
}

func parseJSONValue(raw string, flagName string) (interface{}, error) {
func parseJSONValue(pc *parseCtx, raw string, flagName string) (interface{}, error) {
var err error
raw, err = loadJSONInput(raw, flagName)
raw, err = loadJSONInput(pc, raw, flagName)
if err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions shortcuts/base/base_shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,22 @@ func TestBaseAction(t *testing.T) {
}

func TestParseObjectList(t *testing.T) {
items, err := parseObjectList("", "view")
items, err := parseObjectList(testPC, "", "view")
if err != nil || items != nil {
t.Fatalf("items=%v err=%v", items, err)
}

items, err = parseObjectList(`{"name":"grid"}`, "view")
items, err = parseObjectList(testPC, `{"name":"grid"}`, "view")
if err != nil || len(items) != 1 || items[0]["name"] != "grid" {
t.Fatalf("items=%v err=%v", items, err)
}

items, err = parseObjectList(`[{"name":"grid"}]`, "view")
items, err = parseObjectList(testPC, `[{"name":"grid"}]`, "view")
if err != nil || len(items) != 1 || items[0]["name"] != "grid" {
t.Fatalf("items=%v err=%v", items, err)
}

_, err = parseObjectList(`[1]`, "view")
_, err = parseObjectList(testPC, `[1]`, "view")
if err == nil || !strings.Contains(err.Error(), "must be an object") {
t.Fatalf("err=%v", err)
}
Expand Down
6 changes: 4 additions & 2 deletions shortcuts/base/dashboard_block_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ var BaseDashboardBlockCreate = common.Shortcut{
{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
if runtime.Bool("no-validate") {
return nil
}
raw := runtime.Str("data-config")
if strings.TrimSpace(raw) == "" {
return nil // 允许无 data_config 的创建(某些类型可先创建后配置)
}
cfg, err := parseJSONObject(raw, "data-config")
cfg, err := parseJSONObject(pc, raw, "data-config")
if err != nil {
return err
}
Expand All @@ -50,6 +51,7 @@ var BaseDashboardBlockCreate = common.Shortcut{
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
body := map[string]interface{}{}
if name := runtime.Str("name"); name != "" {
body["name"] = name
Expand All @@ -58,7 +60,7 @@ var BaseDashboardBlockCreate = common.Shortcut{
body["type"] = t
}
if raw := runtime.Str("data-config"); raw != "" {
if parsed, err := parseJSONObject(raw, "data-config"); err == nil {
if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil {
body["data_config"] = parsed
}
}
Expand Down
6 changes: 4 additions & 2 deletions shortcuts/base/dashboard_block_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ var BaseDashboardBlockUpdate = common.Shortcut{
{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
if runtime.Bool("no-validate") {
return nil
}
raw := runtime.Str("data-config")
if strings.TrimSpace(raw) == "" {
return nil
}
cfg, err := parseJSONObject(raw, "data-config")
cfg, err := parseJSONObject(pc, raw, "data-config")
if err != nil {
return err
}
Expand All @@ -49,12 +50,13 @@ var BaseDashboardBlockUpdate = common.Shortcut{
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
body := map[string]interface{}{}
if name := runtime.Str("name"); name != "" {
body["name"] = name
}
if raw := runtime.Str("data-config"); raw != "" {
if parsed, err := parseJSONObject(raw, "data-config"); err == nil {
if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil {
body["data_config"] = parsed
}
}
Expand Down
12 changes: 8 additions & 4 deletions shortcuts/base/dashboard_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func dryRunDashboardBlockGet(_ context.Context, runtime *common.RuntimeContext)
}

func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
body["name"] = name
Expand All @@ -103,7 +104,7 @@ func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContex
body["type"] = blockType
}
if raw := runtime.Str("data-config"); raw != "" {
if parsed, err := parseJSONObject(raw, "data-config"); err == nil {
if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil {
body["data_config"] = parsed
}
}
Expand All @@ -119,12 +120,13 @@ func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContex
}

func dryRunDashboardBlockUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
body["name"] = name
}
if raw := runtime.Str("data-config"); raw != "" {
if parsed, err := parseJSONObject(raw, "data-config"); err == nil {
if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil {
body["data_config"] = parsed
}
}
Expand Down Expand Up @@ -240,6 +242,7 @@ func executeDashboardBlockGet(runtime *common.RuntimeContext) error {
}

func executeDashboardBlockCreate(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
body["name"] = name
Expand All @@ -248,7 +251,7 @@ func executeDashboardBlockCreate(runtime *common.RuntimeContext) error {
body["type"] = blockType
}
if raw := runtime.Str("data-config"); raw != "" {
parsed, err := parseJSONObject(raw, "data-config")
parsed, err := parseJSONObject(pc, raw, "data-config")
if err != nil {
return err
}
Expand All @@ -269,12 +272,13 @@ func executeDashboardBlockCreate(runtime *common.RuntimeContext) error {
}

func executeDashboardBlockUpdate(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
body["name"] = name
}
if raw := runtime.Str("data-config"); raw != "" {
parsed, err := parseJSONObject(raw, "data-config")
parsed, err := parseJSONObject(pc, raw, "data-config")
if err != nil {
return err
}
Expand Down
15 changes: 10 additions & 5 deletions shortcuts/base/field_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ func dryRunFieldGet(_ context.Context, runtime *common.RuntimeContext) *common.D
}

func dryRunFieldCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body, _ := parseJSONObject(runtime.Str("json"), "json")
pc := newParseCtx(runtime)
body, _ := parseJSONObject(pc, runtime.Str("json"), "json")
return common.NewDryRunAPI().
POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields").
Body(body).
Expand All @@ -41,7 +42,8 @@ func dryRunFieldCreate(_ context.Context, runtime *common.RuntimeContext) *commo
}

func dryRunFieldUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body, _ := parseJSONObject(runtime.Str("json"), "json")
pc := newParseCtx(runtime)
body, _ := parseJSONObject(pc, runtime.Str("json"), "json")
return common.NewDryRunAPI().
PUT("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id").
Body(body).
Expand Down Expand Up @@ -78,7 +80,8 @@ func dryRunFieldSearchOptions(_ context.Context, runtime *common.RuntimeContext)
}

func validateFieldJSON(runtime *common.RuntimeContext) (map[string]interface{}, error) {
raw, _ := loadJSONInput(runtime.Str("json"), "json")
pc := newParseCtx(runtime)
raw, _ := loadJSONInput(pc, runtime.Str("json"), "json")
if raw == "" {
return nil, nil
}
Expand Down Expand Up @@ -148,7 +151,8 @@ func executeFieldGet(runtime *common.RuntimeContext) error {
}

func executeFieldCreate(runtime *common.RuntimeContext) error {
body, err := parseJSONObject(runtime.Str("json"), "json")
pc := newParseCtx(runtime)
body, err := parseJSONObject(pc, runtime.Str("json"), "json")
if err != nil {
return err
}
Expand All @@ -161,9 +165,10 @@ func executeFieldCreate(runtime *common.RuntimeContext) error {
}

func executeFieldUpdate(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
baseToken := runtime.Str("base-token")
tableIDValue := baseTableID(runtime)
body, err := parseJSONObject(runtime.Str("json"), "json")
body, err := parseJSONObject(pc, runtime.Str("json"), "json")
if err != nil {
return err
}
Expand Down
27 changes: 19 additions & 8 deletions shortcuts/base/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ type fieldTypeSpec struct {
Extra map[string]interface{}
}

func parseJSONObject(raw string, flagName string) (map[string]interface{}, error) {
resolved, err := loadJSONInput(raw, flagName)
func parseJSONObject(pc *parseCtx, raw string, flagName string) (map[string]interface{}, error) {
resolved, err := loadJSONInput(pc, raw, flagName)
if err != nil {
return nil, err
}
Expand All @@ -41,8 +41,8 @@ func parseJSONObject(raw string, flagName string) (map[string]interface{}, error
return result, nil
}

func parseJSONArray(raw string, flagName string) ([]interface{}, error) {
resolved, err := loadJSONInput(raw, flagName)
func parseJSONArray(pc *parseCtx, raw string, flagName string) ([]interface{}, error) {
resolved, err := loadJSONInput(pc, raw, flagName)
if err != nil {
return nil, err
}
Expand All @@ -53,12 +53,12 @@ func parseJSONArray(raw string, flagName string) ([]interface{}, error) {
return result, nil
}

func parseStringListFlexible(raw string, flagName string) ([]string, error) {
func parseStringListFlexible(pc *parseCtx, raw string, flagName string) ([]string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
resolved, err := loadJSONInput(raw, flagName)
resolved, err := loadJSONInput(pc, raw, flagName)
if err != nil {
return nil, err
}
Expand All @@ -82,8 +82,19 @@ func parseStringListFlexible(raw string, flagName string) ([]string, error) {
}

func parseStringList(raw string) []string {
items, _ := parseStringListFlexible(raw, "fields")
return items
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
item := strings.TrimSpace(part)
if item != "" {
result = append(result, item)
}
}
return result
}

func deepMergeMaps(dst, src map[string]interface{}) map[string]interface{} {
Expand Down
Loading
Loading