From be0d8485e48d92cbe0bed49da68c1beab28f254b Mon Sep 17 00:00:00 2001 From: tuxedomm <273098272+tuxedomm@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:04:02 +0800 Subject: [PATCH 1/5] refactor: migrate base shortcuts to FileIO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loadJSONInput: SafeInputPath + vfs.ReadFile → fio.Open + io.ReadAll - parseJSONObject/parseJSONArray/parseJSONValue/parseObjectList/ parseStringListFlexible: add fio param, pass through to loadJSONInput - parseStringList: inline comma-split (no longer depends on fio) - record_upload_attachment: SafeInputPath + vfs.Stat → FileIO.Stat with ErrPathValidation check; vfs.Open → FileIO.Open - All ops files pass runtime.FileIO() to parse helpers Change-Id: Ie5938d8ad8600e185a7f5019c6e8c51b6b80c988 --- shortcuts/base/base_shortcut_helpers.go | 23 +++++++++-------- shortcuts/base/base_shortcuts_test.go | 8 +++--- shortcuts/base/dashboard_block_create.go | 4 +-- shortcuts/base/dashboard_block_update.go | 4 +-- shortcuts/base/dashboard_ops.go | 8 +++--- shortcuts/base/field_ops.go | 10 ++++---- shortcuts/base/helpers.go | 28 +++++++++++++++------ shortcuts/base/helpers_test.go | 29 +++++++++++++--------- shortcuts/base/record_ops.go | 4 +-- shortcuts/base/record_upload_attachment.go | 16 +++++------- shortcuts/base/table_ops.go | 4 +-- shortcuts/base/view_ops.go | 12 ++++----- shortcuts/base/workflow_create.go | 12 ++++----- shortcuts/base/workflow_update.go | 12 ++++----- 14 files changed, 94 insertions(+), 80 deletions(-) diff --git a/shortcuts/base/base_shortcut_helpers.go b/shortcuts/base/base_shortcut_helpers.go index 06b21e005..3cbbd9980 100644 --- a/shortcuts/base/base_shortcut_helpers.go +++ b/shortcuts/base/base_shortcut_helpers.go @@ -6,10 +6,10 @@ package base import ( "encoding/json" "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" ) @@ -17,7 +17,7 @@ func baseTableID(runtime *common.RuntimeContext) string { return strings.TrimSpace(runtime.Str("table-id")) } -func loadJSONInput(raw string, flagName string) (string, error) { +func loadJSONInput(fio fileio.FileIO, raw string, flagName string) (string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "", common.FlagErrorf("--%s cannot be empty", flagName) @@ -29,11 +29,12 @@ 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) + f, err := fio.Open(path) if err != nil { return "", common.FlagErrorf("--%s invalid JSON file path %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) } @@ -86,18 +87,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(fio fileio.FileIO, 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(fio, raw, flagName) if err != nil { return nil, err } if strings.HasPrefix(raw, "[") { - arr, err := parseJSONArray(raw, flagName) + arr, err := parseJSONArray(fio, raw, flagName) if err != nil { return nil, err } @@ -111,16 +112,16 @@ func parseObjectList(raw string, flagName string) ([]map[string]interface{}, err } return items, nil } - obj, err := parseJSONObject(raw, flagName) + obj, err := parseJSONObject(fio, raw, flagName) if err != nil { return nil, err } return []map[string]interface{}{obj}, nil } -func parseJSONValue(raw string, flagName string) (interface{}, error) { +func parseJSONValue(fio fileio.FileIO, raw string, flagName string) (interface{}, error) { var err error - raw, err = loadJSONInput(raw, flagName) + raw, err = loadJSONInput(fio, raw, flagName) if err != nil { return nil, err } diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index a6f1c61d0..63922f284 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -70,22 +70,22 @@ func TestBaseAction(t *testing.T) { } func TestParseObjectList(t *testing.T) { - items, err := parseObjectList("", "view") + items, err := parseObjectList(testFIO, "", "view") if err != nil || items != nil { t.Fatalf("items=%v err=%v", items, err) } - items, err = parseObjectList(`{"name":"grid"}`, "view") + items, err = parseObjectList(testFIO, `{"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(testFIO, `[{"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(testFIO, `[1]`, "view") if err == nil || !strings.Contains(err.Error(), "must be an object") { t.Fatalf("err=%v", err) } diff --git a/shortcuts/base/dashboard_block_create.go b/shortcuts/base/dashboard_block_create.go index 9b663d33b..831f6f4f6 100644 --- a/shortcuts/base/dashboard_block_create.go +++ b/shortcuts/base/dashboard_block_create.go @@ -36,7 +36,7 @@ var BaseDashboardBlockCreate = common.Shortcut{ if strings.TrimSpace(raw) == "" { return nil // 允许无 data_config 的创建(某些类型可先创建后配置) } - cfg, err := parseJSONObject(raw, "data-config") + cfg, err := parseJSONObject(runtime.FileIO(), raw, "data-config") if err != nil { return err } @@ -58,7 +58,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(runtime.FileIO(), raw, "data-config"); err == nil { body["data_config"] = parsed } } diff --git a/shortcuts/base/dashboard_block_update.go b/shortcuts/base/dashboard_block_update.go index 3a6cc59e0..e5114e3b3 100644 --- a/shortcuts/base/dashboard_block_update.go +++ b/shortcuts/base/dashboard_block_update.go @@ -36,7 +36,7 @@ var BaseDashboardBlockUpdate = common.Shortcut{ if strings.TrimSpace(raw) == "" { return nil } - cfg, err := parseJSONObject(raw, "data-config") + cfg, err := parseJSONObject(runtime.FileIO(), raw, "data-config") if err != nil { return err } @@ -54,7 +54,7 @@ var BaseDashboardBlockUpdate = common.Shortcut{ body["name"] = name } if raw := runtime.Str("data-config"); raw != "" { - if parsed, err := parseJSONObject(raw, "data-config"); err == nil { + if parsed, err := parseJSONObject(runtime.FileIO(), raw, "data-config"); err == nil { body["data_config"] = parsed } } diff --git a/shortcuts/base/dashboard_ops.go b/shortcuts/base/dashboard_ops.go index c319fde59..21bb571df 100644 --- a/shortcuts/base/dashboard_ops.go +++ b/shortcuts/base/dashboard_ops.go @@ -103,7 +103,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(runtime.FileIO(), raw, "data-config"); err == nil { body["data_config"] = parsed } } @@ -124,7 +124,7 @@ func dryRunDashboardBlockUpdate(_ context.Context, runtime *common.RuntimeContex body["name"] = name } if raw := runtime.Str("data-config"); raw != "" { - if parsed, err := parseJSONObject(raw, "data-config"); err == nil { + if parsed, err := parseJSONObject(runtime.FileIO(), raw, "data-config"); err == nil { body["data_config"] = parsed } } @@ -248,7 +248,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(runtime.FileIO(), raw, "data-config") if err != nil { return err } @@ -274,7 +274,7 @@ func executeDashboardBlockUpdate(runtime *common.RuntimeContext) error { body["name"] = name } if raw := runtime.Str("data-config"); raw != "" { - parsed, err := parseJSONObject(raw, "data-config") + parsed, err := parseJSONObject(runtime.FileIO(), raw, "data-config") if err != nil { return err } diff --git a/shortcuts/base/field_ops.go b/shortcuts/base/field_ops.go index 0976c90d2..5b3ed20de 100644 --- a/shortcuts/base/field_ops.go +++ b/shortcuts/base/field_ops.go @@ -32,7 +32,7 @@ func dryRunFieldGet(_ context.Context, runtime *common.RuntimeContext) *common.D } func dryRunFieldCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.Str("json"), "json") + body, _ := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") return common.NewDryRunAPI(). POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields"). Body(body). @@ -41,7 +41,7 @@ func dryRunFieldCreate(_ context.Context, runtime *common.RuntimeContext) *commo } func dryRunFieldUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.Str("json"), "json") + body, _ := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") return common.NewDryRunAPI(). PUT("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id"). Body(body). @@ -78,7 +78,7 @@ func dryRunFieldSearchOptions(_ context.Context, runtime *common.RuntimeContext) } func validateFieldJSON(runtime *common.RuntimeContext) (map[string]interface{}, error) { - raw, _ := loadJSONInput(runtime.Str("json"), "json") + raw, _ := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") if raw == "" { return nil, nil } @@ -148,7 +148,7 @@ func executeFieldGet(runtime *common.RuntimeContext) error { } func executeFieldCreate(runtime *common.RuntimeContext) error { - body, err := parseJSONObject(runtime.Str("json"), "json") + body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } @@ -163,7 +163,7 @@ func executeFieldCreate(runtime *common.RuntimeContext) error { func executeFieldUpdate(runtime *common.RuntimeContext) error { baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) - body, err := parseJSONObject(runtime.Str("json"), "json") + body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } diff --git a/shortcuts/base/helpers.go b/shortcuts/base/helpers.go index 9032ab5a2..49df26242 100644 --- a/shortcuts/base/helpers.go +++ b/shortcuts/base/helpers.go @@ -16,6 +16,7 @@ import ( larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + "github.com/larksuite/cli/extension/fileio" "github.com/larksuite/cli/shortcuts/common" ) @@ -29,8 +30,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(fio fileio.FileIO, raw string, flagName string) (map[string]interface{}, error) { + resolved, err := loadJSONInput(fio, raw, flagName) if err != nil { return nil, err } @@ -41,8 +42,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(fio fileio.FileIO, raw string, flagName string) ([]interface{}, error) { + resolved, err := loadJSONInput(fio, raw, flagName) if err != nil { return nil, err } @@ -53,12 +54,12 @@ func parseJSONArray(raw string, flagName string) ([]interface{}, error) { return result, nil } -func parseStringListFlexible(raw string, flagName string) ([]string, error) { +func parseStringListFlexible(fio fileio.FileIO, raw string, flagName string) ([]string, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, nil } - resolved, err := loadJSONInput(raw, flagName) + resolved, err := loadJSONInput(fio, raw, flagName) if err != nil { return nil, err } @@ -82,8 +83,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{} { diff --git a/shortcuts/base/helpers_test.go b/shortcuts/base/helpers_test.go index 61806c4ae..536588d3b 100644 --- a/shortcuts/base/helpers_test.go +++ b/shortcuts/base/helpers_test.go @@ -10,8 +10,13 @@ import ( "strings" "testing" "time" + + "github.com/larksuite/cli/extension/fileio" + "github.com/larksuite/cli/internal/vfs/localfileio" ) +var testFIO fileio.FileIO = &localfileio.LocalFileIO{} + func TestParseHelpers(t *testing.T) { tmpDir := t.TempDir() cwd, err := os.Getwd() @@ -30,36 +35,36 @@ func TestParseHelpers(t *testing.T) { t.Fatalf("write temp file err=%v", err) } _ = tmp.Close() - obj, err := parseJSONObject(`{"name":"demo"}`, "json") + obj, err := parseJSONObject(testFIO, `{"name":"demo"}`, "json") if err != nil || obj["name"] != "demo" { t.Fatalf("obj=%v err=%v", obj, err) } - if _, err := parseJSONObject(`[1]`, "json"); err == nil || !strings.Contains(err.Error(), "invalid JSON object") { + if _, err := parseJSONObject(testFIO, `[1]`, "json"); err == nil || !strings.Contains(err.Error(), "invalid JSON object") { t.Fatalf("err=%v", err) } - obj, err = parseJSONObject("@"+tmp.Name(), "json") + obj, err = parseJSONObject(testFIO, "@"+tmp.Name(), "json") if err != nil || obj["name"] != "from-file" { t.Fatalf("file obj=%v err=%v", obj, err) } - arr, err := parseJSONArray(`[1,2]`, "items") + arr, err := parseJSONArray(testFIO, `[1,2]`, "items") if err != nil || len(arr) != 2 { t.Fatalf("arr=%v err=%v", arr, err) } - if _, err := parseJSONArray(`{"a":1}`, "items"); err == nil || !strings.Contains(err.Error(), "invalid JSON array") { + if _, err := parseJSONArray(testFIO, `{"a":1}`, "items"); err == nil || !strings.Contains(err.Error(), "invalid JSON array") { t.Fatalf("err=%v", err) } - list, err := parseStringListFlexible("a, b, ,c", "fields") + list, err := parseStringListFlexible(testFIO, "a, b, ,c", "fields") if err != nil || !reflect.DeepEqual(list, []string{"a", "b", "c"}) { t.Fatalf("list=%v err=%v", list, err) } - list, err = parseStringListFlexible(`["x","y"]`, "fields") + list, err = parseStringListFlexible(testFIO, `["x","y"]`, "fields") if err != nil || !reflect.DeepEqual(list, []string{"x", "y"}) { t.Fatalf("list=%v err=%v", list, err) } - if _, err := parseStringListFlexible(`[1]`, "fields"); err == nil || !strings.Contains(err.Error(), "invalid JSON string array") { + if _, err := parseStringListFlexible(testFIO, `[1]`, "fields"); err == nil || !strings.Contains(err.Error(), "invalid JSON string array") { t.Fatalf("err=%v", err) } - if _, err := parseJSONValue("{", "json"); err == nil || !strings.Contains(err.Error(), "tip: pass a JSON object/array directly") { + if _, err := parseJSONValue(testFIO, "{", "json"); err == nil || !strings.Contains(err.Error(), "tip: pass a JSON object/array directly") { t.Fatalf("err=%v", err) } if !reflect.DeepEqual(parseStringList("m,n"), []string{"m", "n"}) { @@ -262,10 +267,10 @@ func TestFilterAndSortHelpers(t *testing.T) { } func TestJSONInputHelpers(t *testing.T) { - if got, err := loadJSONInput(`{"name":"demo"}`, "json"); err != nil || got != `{"name":"demo"}` { + if got, err := loadJSONInput(testFIO, `{"name":"demo"}`, "json"); err != nil || got != `{"name":"demo"}` { t.Fatalf("got=%q err=%v", got, err) } - if _, err := loadJSONInput("@", "json"); err == nil || !strings.Contains(err.Error(), "file path cannot be empty") { + if _, err := loadJSONInput(testFIO, "@", "json"); err == nil || !strings.Contains(err.Error(), "file path cannot be empty") { t.Fatalf("err=%v", err) } tmp := t.TempDir() @@ -281,7 +286,7 @@ func TestJSONInputHelpers(t *testing.T) { if err := os.WriteFile(emptyPath, []byte(" \n"), 0o644); err != nil { t.Fatalf("write empty file err=%v", err) } - if _, err := loadJSONInput("@"+emptyPath, "json"); err == nil || !strings.Contains(err.Error(), "is empty") { + if _, err := loadJSONInput(testFIO, "@"+emptyPath, "json"); err == nil || !strings.Contains(err.Error(), "is empty") { t.Fatalf("err=%v", err) } syntaxErr := formatJSONError("json", "object", &json.SyntaxError{Offset: 7}) diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 280b1c580..828097466 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -35,7 +35,7 @@ func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common. } func dryRunRecordUpsert(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.Str("json"), "json") + body, _ := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") if recordID := runtime.Str("record-id"); recordID != "" { return common.NewDryRunAPI(). PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id"). @@ -106,7 +106,7 @@ func executeRecordGet(runtime *common.RuntimeContext) error { } func executeRecordUpsert(runtime *common.RuntimeContext) error { - body, err := parseJSONObject(runtime.Str("json"), "json") + body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } diff --git a/shortcuts/base/record_upload_attachment.go b/shortcuts/base/record_upload_attachment.go index 31d0ffc51..fbf3f2456 100644 --- a/shortcuts/base/record_upload_attachment.go +++ b/shortcuts/base/record_upload_attachment.go @@ -14,10 +14,9 @@ import ( larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + "github.com/larksuite/cli/extension/fileio" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/util" - "github.com/larksuite/cli/internal/validate" - "github.com/larksuite/cli/internal/vfs" "github.com/larksuite/cli/shortcuts/common" ) @@ -91,14 +90,11 @@ func dryRunRecordUploadAttachment(_ context.Context, runtime *common.RuntimeCont func executeRecordUploadAttachment(runtime *common.RuntimeContext) error { filePath := runtime.Str("file") - safeFilePath, err := validate.SafeInputPath(filePath) - if err != nil { - return output.ErrValidation("unsafe file path: %s", err) - } - filePath = safeFilePath - - fileInfo, err := vfs.Stat(filePath) + fileInfo, err := runtime.FileIO().Stat(filePath) if err != nil { + if errors.Is(err, fileio.ErrPathValidation) { + return output.ErrValidation("unsafe file path: %s", err) + } return output.ErrValidation("file not found: %s", filePath) } if fileInfo.Size() > baseAttachmentUploadMaxFileSize { @@ -209,7 +205,7 @@ func normalizeAttachmentForPatch(attachment map[string]interface{}) map[string]i } func uploadAttachmentToBase(runtime *common.RuntimeContext, filePath, fileName, baseToken string, fileSize int64) (map[string]interface{}, error) { - f, err := vfs.Open(filePath) + f, err := runtime.FileIO().Open(filePath) if err != nil { return nil, output.ErrValidation("cannot open file: %v", err) } diff --git a/shortcuts/base/table_ops.go b/shortcuts/base/table_ops.go index 044823965..b613de8f0 100644 --- a/shortcuts/base/table_ops.go +++ b/shortcuts/base/table_ops.go @@ -108,7 +108,7 @@ func executeTableCreate(runtime *common.RuntimeContext) error { result := map[string]interface{}{"table": created} tableIDValue := tableID(created) if tableIDValue != "" && runtime.Str("fields") != "" { - fieldItems, err := parseJSONArray(runtime.Str("fields"), "fields") + fieldItems, err := parseJSONArray(runtime.FileIO(), runtime.Str("fields"), "fields") if err != nil { return err } @@ -139,7 +139,7 @@ func executeTableCreate(runtime *common.RuntimeContext) error { result["fields"] = createdFields } if tableIDValue != "" && runtime.Str("view") != "" { - viewItems, err := parseObjectList(runtime.Str("view"), "view") + viewItems, err := parseObjectList(runtime.FileIO(), runtime.Str("view"), "view") if err != nil { return err } diff --git a/shortcuts/base/view_ops.go b/shortcuts/base/view_ops.go index 53211cf1c..0e8ec3980 100644 --- a/shortcuts/base/view_ops.go +++ b/shortcuts/base/view_ops.go @@ -36,7 +36,7 @@ func dryRunViewGet(_ context.Context, runtime *common.RuntimeContext) *common.Dr func dryRunViewCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { api := dryRunViewBase(runtime) - bodyList, err := parseObjectList(runtime.Str("json"), "json") + bodyList, err := parseObjectList(runtime.FileIO(), runtime.Str("json"), "json") if err != nil || len(bodyList) == 0 { return api.POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/views") } @@ -57,14 +57,14 @@ func dryRunViewGetProperty(runtime *common.RuntimeContext, segment string) *comm } func dryRunViewSetJSONObject(runtime *common.RuntimeContext, segment string) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.Str("json"), "json") + body, _ := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") return dryRunViewBase(runtime). PUT(fmt.Sprintf("/open-apis/base/v3/bases/:base_token/tables/:table_id/views/:view_id/%s", url.PathEscape(segment))). Body(body) } func dryRunViewSetWrapped(runtime *common.RuntimeContext, segment string, wrapper string) *common.DryRunAPI { - raw, err := parseJSONValue(runtime.Str("json"), "json") + raw, err := parseJSONValue(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { raw = nil } @@ -170,7 +170,7 @@ func executeViewGet(runtime *common.RuntimeContext) error { func executeViewCreate(runtime *common.RuntimeContext) error { baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) - viewItems, err := parseObjectList(runtime.Str("json"), "json") + viewItems, err := parseObjectList(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } @@ -214,7 +214,7 @@ func executeViewSetJSONObject(runtime *common.RuntimeContext, segment string, ke baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) viewRef := runtime.Str("view-id") - body, err := parseJSONObject(runtime.Str("json"), "json") + body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } @@ -230,7 +230,7 @@ func executeViewSetWrapped(runtime *common.RuntimeContext, segment string, wrapp baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) viewRef := runtime.Str("view-id") - raw, err := parseJSONValue(runtime.Str("json"), "json") + raw, err := parseJSONValue(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } diff --git a/shortcuts/base/workflow_create.go b/shortcuts/base/workflow_create.go index 0bba5bbeb..da953a5c9 100644 --- a/shortcuts/base/workflow_create.go +++ b/shortcuts/base/workflow_create.go @@ -25,19 +25,19 @@ var BaseWorkflowCreate = common.Shortcut{ if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") } - raw, err := loadJSONInput(runtime.Str("json"), "json") + raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } - if _, err := parseJSONObject(raw, "json"); err != nil { + if _, err := parseJSONObject(runtime.FileIO(), raw, "json"); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { var body map[string]interface{} - if raw, err := loadJSONInput(runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(raw, "json") + if raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json"); err == nil { + body, _ = parseJSONObject(runtime.FileIO(), raw, "json") } return common.NewDryRunAPI(). POST("/open-apis/base/v3/bases/:base_token/workflows"). @@ -45,11 +45,11 @@ var BaseWorkflowCreate = common.Shortcut{ Set("base_token", runtime.Str("base-token")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - raw, err := loadJSONInput(runtime.Str("json"), "json") + raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } - body, err := parseJSONObject(raw, "json") + body, err := parseJSONObject(runtime.FileIO(), raw, "json") if err != nil { return err } diff --git a/shortcuts/base/workflow_update.go b/shortcuts/base/workflow_update.go index 0b316e4fd..27fe96a58 100644 --- a/shortcuts/base/workflow_update.go +++ b/shortcuts/base/workflow_update.go @@ -29,19 +29,19 @@ var BaseWorkflowUpdate = common.Shortcut{ if strings.TrimSpace(runtime.Str("workflow-id")) == "" { return common.FlagErrorf("--workflow-id must not be blank") } - raw, err := loadJSONInput(runtime.Str("json"), "json") + raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } - if _, err := parseJSONObject(raw, "json"); err != nil { + if _, err := parseJSONObject(runtime.FileIO(), raw, "json"); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { var body map[string]interface{} - if raw, err := loadJSONInput(runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(raw, "json") + if raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json"); err == nil { + body, _ = parseJSONObject(runtime.FileIO(), raw, "json") } return common.NewDryRunAPI(). PUT("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id"). @@ -50,11 +50,11 @@ var BaseWorkflowUpdate = common.Shortcut{ Set("workflow_id", runtime.Str("workflow-id")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - raw, err := loadJSONInput(runtime.Str("json"), "json") + raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") if err != nil { return err } - body, err := parseJSONObject(raw, "json") + body, err := parseJSONObject(runtime.FileIO(), raw, "json") if err != nil { return err } From b06ee5799e0d8b192bee2d35f1ae3303c17dfc04 Mon Sep 17 00:00:00 2001 From: tuxedomm <273098272+tuxedomm@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:22:50 +0800 Subject: [PATCH 2/5] refactor: cache runtime.FileIO() in local variable Reduce visual noise in functions that call runtime.FileIO() multiple times by caching into a local `fio` variable. Change-Id: I447215b9f48483fd43b559b5801baf0d046ec59b --- shortcuts/base/table_ops.go | 5 +++-- shortcuts/base/workflow_create.go | 15 +++++++++------ shortcuts/base/workflow_update.go | 15 +++++++++------ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/shortcuts/base/table_ops.go b/shortcuts/base/table_ops.go index b613de8f0..d3972ce8b 100644 --- a/shortcuts/base/table_ops.go +++ b/shortcuts/base/table_ops.go @@ -107,8 +107,9 @@ func executeTableCreate(runtime *common.RuntimeContext) error { } result := map[string]interface{}{"table": created} tableIDValue := tableID(created) + fio := runtime.FileIO() if tableIDValue != "" && runtime.Str("fields") != "" { - fieldItems, err := parseJSONArray(runtime.FileIO(), runtime.Str("fields"), "fields") + fieldItems, err := parseJSONArray(fio, runtime.Str("fields"), "fields") if err != nil { return err } @@ -139,7 +140,7 @@ func executeTableCreate(runtime *common.RuntimeContext) error { result["fields"] = createdFields } if tableIDValue != "" && runtime.Str("view") != "" { - viewItems, err := parseObjectList(runtime.FileIO(), runtime.Str("view"), "view") + viewItems, err := parseObjectList(fio, runtime.Str("view"), "view") if err != nil { return err } diff --git a/shortcuts/base/workflow_create.go b/shortcuts/base/workflow_create.go index da953a5c9..0684c4e0c 100644 --- a/shortcuts/base/workflow_create.go +++ b/shortcuts/base/workflow_create.go @@ -25,19 +25,21 @@ var BaseWorkflowCreate = common.Shortcut{ if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") } - raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") + fio := runtime.FileIO() + raw, err := loadJSONInput(fio, runtime.Str("json"), "json") if err != nil { return err } - if _, err := parseJSONObject(runtime.FileIO(), raw, "json"); err != nil { + if _, err := parseJSONObject(fio, raw, "json"); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + fio := runtime.FileIO() var body map[string]interface{} - if raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(runtime.FileIO(), raw, "json") + if raw, err := loadJSONInput(fio, runtime.Str("json"), "json"); err == nil { + body, _ = parseJSONObject(fio, raw, "json") } return common.NewDryRunAPI(). POST("/open-apis/base/v3/bases/:base_token/workflows"). @@ -45,11 +47,12 @@ var BaseWorkflowCreate = common.Shortcut{ Set("base_token", runtime.Str("base-token")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") + fio := runtime.FileIO() + raw, err := loadJSONInput(fio, runtime.Str("json"), "json") if err != nil { return err } - body, err := parseJSONObject(runtime.FileIO(), raw, "json") + body, err := parseJSONObject(fio, raw, "json") if err != nil { return err } diff --git a/shortcuts/base/workflow_update.go b/shortcuts/base/workflow_update.go index 27fe96a58..135090657 100644 --- a/shortcuts/base/workflow_update.go +++ b/shortcuts/base/workflow_update.go @@ -29,19 +29,21 @@ var BaseWorkflowUpdate = common.Shortcut{ if strings.TrimSpace(runtime.Str("workflow-id")) == "" { return common.FlagErrorf("--workflow-id must not be blank") } - raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") + fio := runtime.FileIO() + raw, err := loadJSONInput(fio, runtime.Str("json"), "json") if err != nil { return err } - if _, err := parseJSONObject(runtime.FileIO(), raw, "json"); err != nil { + if _, err := parseJSONObject(fio, raw, "json"); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + fio := runtime.FileIO() var body map[string]interface{} - if raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(runtime.FileIO(), raw, "json") + if raw, err := loadJSONInput(fio, runtime.Str("json"), "json"); err == nil { + body, _ = parseJSONObject(fio, raw, "json") } return common.NewDryRunAPI(). PUT("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id"). @@ -50,11 +52,12 @@ var BaseWorkflowUpdate = common.Shortcut{ Set("workflow_id", runtime.Str("workflow-id")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - raw, err := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") + fio := runtime.FileIO() + raw, err := loadJSONInput(fio, runtime.Str("json"), "json") if err != nil { return err } - body, err := parseJSONObject(runtime.FileIO(), raw, "json") + body, err := parseJSONObject(fio, raw, "json") if err != nil { return err } From a63ffd52a131e7b06baa3cf6ed0a53e9618f5fdb Mon Sep 17 00:00:00 2001 From: tuxedomm <273098272+tuxedomm@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:27:31 +0800 Subject: [PATCH 3/5] refactor: introduce parseCtx to carry FileIO dependency Replace explicit `fio fileio.FileIO` parameter with `pc *parseCtx` across all JSON/file parsing helpers. Callers create a parseCtx once via `newParseCtx(runtime)` and pass it through. This makes the dependency injection point extensible (e.g. adding logger, context) without changing function signatures again. Change-Id: I6155c83cb3a41655c73013b2042935ac6a7b16ac --- shortcuts/base/base_shortcut_helpers.go | 25 +++++++++++++++------- shortcuts/base/base_shortcuts_test.go | 8 +++---- shortcuts/base/dashboard_block_create.go | 6 ++++-- shortcuts/base/dashboard_block_update.go | 6 ++++-- shortcuts/base/dashboard_ops.go | 12 +++++++---- shortcuts/base/field_ops.go | 15 ++++++++----- shortcuts/base/helpers.go | 13 ++++++------ shortcuts/base/helpers_test.go | 27 ++++++++++++------------ shortcuts/base/record_ops.go | 6 ++++-- shortcuts/base/table_ops.go | 6 +++--- shortcuts/base/view_ops.go | 18 ++++++++++------ shortcuts/base/workflow_create.go | 18 ++++++++-------- shortcuts/base/workflow_update.go | 18 ++++++++-------- 13 files changed, 103 insertions(+), 75 deletions(-) diff --git a/shortcuts/base/base_shortcut_helpers.go b/shortcuts/base/base_shortcut_helpers.go index 3cbbd9980..7424392e6 100644 --- a/shortcuts/base/base_shortcut_helpers.go +++ b/shortcuts/base/base_shortcut_helpers.go @@ -13,11 +13,20 @@ import ( "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(fio fileio.FileIO, 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) @@ -29,7 +38,7 @@ func loadJSONInput(fio fileio.FileIO, raw string, flagName string) (string, erro if path == "" { return "", common.FlagErrorf("--%s file path cannot be empty after @", flagName) } - f, err := fio.Open(path) + f, err := pc.fio.Open(path) if err != nil { return "", common.FlagErrorf("--%s invalid JSON file path %q: %v", flagName, path, err) } @@ -87,18 +96,18 @@ func baseAction(runtime *common.RuntimeContext, boolFlags []string, stringFlags return active[0], nil } -func parseObjectList(fio fileio.FileIO, 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(fio, raw, flagName) + raw, err = loadJSONInput(pc, raw, flagName) if err != nil { return nil, err } if strings.HasPrefix(raw, "[") { - arr, err := parseJSONArray(fio, raw, flagName) + arr, err := parseJSONArray(pc, raw, flagName) if err != nil { return nil, err } @@ -112,16 +121,16 @@ func parseObjectList(fio fileio.FileIO, raw string, flagName string) ([]map[stri } return items, nil } - obj, err := parseJSONObject(fio, raw, flagName) + obj, err := parseJSONObject(pc, raw, flagName) if err != nil { return nil, err } return []map[string]interface{}{obj}, nil } -func parseJSONValue(fio fileio.FileIO, raw string, flagName string) (interface{}, error) { +func parseJSONValue(pc *parseCtx, raw string, flagName string) (interface{}, error) { var err error - raw, err = loadJSONInput(fio, raw, flagName) + raw, err = loadJSONInput(pc, raw, flagName) if err != nil { return nil, err } diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 63922f284..001aaf7ba 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -70,22 +70,22 @@ func TestBaseAction(t *testing.T) { } func TestParseObjectList(t *testing.T) { - items, err := parseObjectList(testFIO, "", "view") + items, err := parseObjectList(testPC, "", "view") if err != nil || items != nil { t.Fatalf("items=%v err=%v", items, err) } - items, err = parseObjectList(testFIO, `{"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(testFIO, `[{"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(testFIO, `[1]`, "view") + _, err = parseObjectList(testPC, `[1]`, "view") if err == nil || !strings.Contains(err.Error(), "must be an object") { t.Fatalf("err=%v", err) } diff --git a/shortcuts/base/dashboard_block_create.go b/shortcuts/base/dashboard_block_create.go index 831f6f4f6..12a0efe79 100644 --- a/shortcuts/base/dashboard_block_create.go +++ b/shortcuts/base/dashboard_block_create.go @@ -29,6 +29,7 @@ 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 } @@ -36,7 +37,7 @@ var BaseDashboardBlockCreate = common.Shortcut{ if strings.TrimSpace(raw) == "" { return nil // 允许无 data_config 的创建(某些类型可先创建后配置) } - cfg, err := parseJSONObject(runtime.FileIO(), raw, "data-config") + cfg, err := parseJSONObject(pc, raw, "data-config") if err != nil { return err } @@ -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 @@ -58,7 +60,7 @@ var BaseDashboardBlockCreate = common.Shortcut{ body["type"] = t } if raw := runtime.Str("data-config"); raw != "" { - if parsed, err := parseJSONObject(runtime.FileIO(), raw, "data-config"); err == nil { + if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil { body["data_config"] = parsed } } diff --git a/shortcuts/base/dashboard_block_update.go b/shortcuts/base/dashboard_block_update.go index e5114e3b3..55af8f1c0 100644 --- a/shortcuts/base/dashboard_block_update.go +++ b/shortcuts/base/dashboard_block_update.go @@ -29,6 +29,7 @@ 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 } @@ -36,7 +37,7 @@ var BaseDashboardBlockUpdate = common.Shortcut{ if strings.TrimSpace(raw) == "" { return nil } - cfg, err := parseJSONObject(runtime.FileIO(), raw, "data-config") + cfg, err := parseJSONObject(pc, raw, "data-config") if err != nil { return err } @@ -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(runtime.FileIO(), raw, "data-config"); err == nil { + if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil { body["data_config"] = parsed } } diff --git a/shortcuts/base/dashboard_ops.go b/shortcuts/base/dashboard_ops.go index 21bb571df..12dca1d7a 100644 --- a/shortcuts/base/dashboard_ops.go +++ b/shortcuts/base/dashboard_ops.go @@ -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 @@ -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(runtime.FileIO(), raw, "data-config"); err == nil { + if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil { body["data_config"] = parsed } } @@ -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(runtime.FileIO(), raw, "data-config"); err == nil { + if parsed, err := parseJSONObject(pc, raw, "data-config"); err == nil { body["data_config"] = parsed } } @@ -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 @@ -248,7 +251,7 @@ func executeDashboardBlockCreate(runtime *common.RuntimeContext) error { body["type"] = blockType } if raw := runtime.Str("data-config"); raw != "" { - parsed, err := parseJSONObject(runtime.FileIO(), raw, "data-config") + parsed, err := parseJSONObject(pc, raw, "data-config") if err != nil { return err } @@ -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(runtime.FileIO(), raw, "data-config") + parsed, err := parseJSONObject(pc, raw, "data-config") if err != nil { return err } diff --git a/shortcuts/base/field_ops.go b/shortcuts/base/field_ops.go index 5b3ed20de..7daaa6dbd 100644 --- a/shortcuts/base/field_ops.go +++ b/shortcuts/base/field_ops.go @@ -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.FileIO(), 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). @@ -41,7 +42,8 @@ func dryRunFieldCreate(_ context.Context, runtime *common.RuntimeContext) *commo } func dryRunFieldUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.FileIO(), 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). @@ -78,7 +80,8 @@ func dryRunFieldSearchOptions(_ context.Context, runtime *common.RuntimeContext) } func validateFieldJSON(runtime *common.RuntimeContext) (map[string]interface{}, error) { - raw, _ := loadJSONInput(runtime.FileIO(), runtime.Str("json"), "json") + pc := newParseCtx(runtime) + raw, _ := loadJSONInput(pc, runtime.Str("json"), "json") if raw == "" { return nil, nil } @@ -148,7 +151,8 @@ func executeFieldGet(runtime *common.RuntimeContext) error { } func executeFieldCreate(runtime *common.RuntimeContext) error { - body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") + pc := newParseCtx(runtime) + body, err := parseJSONObject(pc, runtime.Str("json"), "json") if err != nil { return err } @@ -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.FileIO(), runtime.Str("json"), "json") + body, err := parseJSONObject(pc, runtime.Str("json"), "json") if err != nil { return err } diff --git a/shortcuts/base/helpers.go b/shortcuts/base/helpers.go index 49df26242..915c88265 100644 --- a/shortcuts/base/helpers.go +++ b/shortcuts/base/helpers.go @@ -16,7 +16,6 @@ import ( larkcore "github.com/larksuite/oapi-sdk-go/v3/core" - "github.com/larksuite/cli/extension/fileio" "github.com/larksuite/cli/shortcuts/common" ) @@ -30,8 +29,8 @@ type fieldTypeSpec struct { Extra map[string]interface{} } -func parseJSONObject(fio fileio.FileIO, raw string, flagName string) (map[string]interface{}, error) { - resolved, err := loadJSONInput(fio, 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 } @@ -42,8 +41,8 @@ func parseJSONObject(fio fileio.FileIO, raw string, flagName string) (map[string return result, nil } -func parseJSONArray(fio fileio.FileIO, raw string, flagName string) ([]interface{}, error) { - resolved, err := loadJSONInput(fio, raw, flagName) +func parseJSONArray(pc *parseCtx, raw string, flagName string) ([]interface{}, error) { + resolved, err := loadJSONInput(pc, raw, flagName) if err != nil { return nil, err } @@ -54,12 +53,12 @@ func parseJSONArray(fio fileio.FileIO, raw string, flagName string) ([]interface return result, nil } -func parseStringListFlexible(fio fileio.FileIO, 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(fio, raw, flagName) + resolved, err := loadJSONInput(pc, raw, flagName) if err != nil { return nil, err } diff --git a/shortcuts/base/helpers_test.go b/shortcuts/base/helpers_test.go index 536588d3b..2640998ae 100644 --- a/shortcuts/base/helpers_test.go +++ b/shortcuts/base/helpers_test.go @@ -11,11 +11,10 @@ import ( "testing" "time" - "github.com/larksuite/cli/extension/fileio" "github.com/larksuite/cli/internal/vfs/localfileio" ) -var testFIO fileio.FileIO = &localfileio.LocalFileIO{} +var testPC = &parseCtx{fio: &localfileio.LocalFileIO{}} func TestParseHelpers(t *testing.T) { tmpDir := t.TempDir() @@ -35,36 +34,36 @@ func TestParseHelpers(t *testing.T) { t.Fatalf("write temp file err=%v", err) } _ = tmp.Close() - obj, err := parseJSONObject(testFIO, `{"name":"demo"}`, "json") + obj, err := parseJSONObject(testPC, `{"name":"demo"}`, "json") if err != nil || obj["name"] != "demo" { t.Fatalf("obj=%v err=%v", obj, err) } - if _, err := parseJSONObject(testFIO, `[1]`, "json"); err == nil || !strings.Contains(err.Error(), "invalid JSON object") { + if _, err := parseJSONObject(testPC, `[1]`, "json"); err == nil || !strings.Contains(err.Error(), "invalid JSON object") { t.Fatalf("err=%v", err) } - obj, err = parseJSONObject(testFIO, "@"+tmp.Name(), "json") + obj, err = parseJSONObject(testPC, "@"+tmp.Name(), "json") if err != nil || obj["name"] != "from-file" { t.Fatalf("file obj=%v err=%v", obj, err) } - arr, err := parseJSONArray(testFIO, `[1,2]`, "items") + arr, err := parseJSONArray(testPC, `[1,2]`, "items") if err != nil || len(arr) != 2 { t.Fatalf("arr=%v err=%v", arr, err) } - if _, err := parseJSONArray(testFIO, `{"a":1}`, "items"); err == nil || !strings.Contains(err.Error(), "invalid JSON array") { + if _, err := parseJSONArray(testPC, `{"a":1}`, "items"); err == nil || !strings.Contains(err.Error(), "invalid JSON array") { t.Fatalf("err=%v", err) } - list, err := parseStringListFlexible(testFIO, "a, b, ,c", "fields") + list, err := parseStringListFlexible(testPC, "a, b, ,c", "fields") if err != nil || !reflect.DeepEqual(list, []string{"a", "b", "c"}) { t.Fatalf("list=%v err=%v", list, err) } - list, err = parseStringListFlexible(testFIO, `["x","y"]`, "fields") + list, err = parseStringListFlexible(testPC, `["x","y"]`, "fields") if err != nil || !reflect.DeepEqual(list, []string{"x", "y"}) { t.Fatalf("list=%v err=%v", list, err) } - if _, err := parseStringListFlexible(testFIO, `[1]`, "fields"); err == nil || !strings.Contains(err.Error(), "invalid JSON string array") { + if _, err := parseStringListFlexible(testPC, `[1]`, "fields"); err == nil || !strings.Contains(err.Error(), "invalid JSON string array") { t.Fatalf("err=%v", err) } - if _, err := parseJSONValue(testFIO, "{", "json"); err == nil || !strings.Contains(err.Error(), "tip: pass a JSON object/array directly") { + if _, err := parseJSONValue(testPC, "{", "json"); err == nil || !strings.Contains(err.Error(), "tip: pass a JSON object/array directly") { t.Fatalf("err=%v", err) } if !reflect.DeepEqual(parseStringList("m,n"), []string{"m", "n"}) { @@ -267,10 +266,10 @@ func TestFilterAndSortHelpers(t *testing.T) { } func TestJSONInputHelpers(t *testing.T) { - if got, err := loadJSONInput(testFIO, `{"name":"demo"}`, "json"); err != nil || got != `{"name":"demo"}` { + if got, err := loadJSONInput(testPC, `{"name":"demo"}`, "json"); err != nil || got != `{"name":"demo"}` { t.Fatalf("got=%q err=%v", got, err) } - if _, err := loadJSONInput(testFIO, "@", "json"); err == nil || !strings.Contains(err.Error(), "file path cannot be empty") { + if _, err := loadJSONInput(testPC, "@", "json"); err == nil || !strings.Contains(err.Error(), "file path cannot be empty") { t.Fatalf("err=%v", err) } tmp := t.TempDir() @@ -286,7 +285,7 @@ func TestJSONInputHelpers(t *testing.T) { if err := os.WriteFile(emptyPath, []byte(" \n"), 0o644); err != nil { t.Fatalf("write empty file err=%v", err) } - if _, err := loadJSONInput(testFIO, "@"+emptyPath, "json"); err == nil || !strings.Contains(err.Error(), "is empty") { + if _, err := loadJSONInput(testPC, "@"+emptyPath, "json"); err == nil || !strings.Contains(err.Error(), "is empty") { t.Fatalf("err=%v", err) } syntaxErr := formatJSONError("json", "object", &json.SyntaxError{Offset: 7}) diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 828097466..bcaec01f5 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -35,7 +35,8 @@ func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common. } func dryRunRecordUpsert(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") + pc := newParseCtx(runtime) + body, _ := parseJSONObject(pc, runtime.Str("json"), "json") if recordID := runtime.Str("record-id"); recordID != "" { return common.NewDryRunAPI(). PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id"). @@ -106,7 +107,8 @@ func executeRecordGet(runtime *common.RuntimeContext) error { } func executeRecordUpsert(runtime *common.RuntimeContext) error { - body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") + pc := newParseCtx(runtime) + body, err := parseJSONObject(pc, runtime.Str("json"), "json") if err != nil { return err } diff --git a/shortcuts/base/table_ops.go b/shortcuts/base/table_ops.go index d3972ce8b..d44b9c9f2 100644 --- a/shortcuts/base/table_ops.go +++ b/shortcuts/base/table_ops.go @@ -107,9 +107,9 @@ func executeTableCreate(runtime *common.RuntimeContext) error { } result := map[string]interface{}{"table": created} tableIDValue := tableID(created) - fio := runtime.FileIO() + pc := newParseCtx(runtime) if tableIDValue != "" && runtime.Str("fields") != "" { - fieldItems, err := parseJSONArray(fio, runtime.Str("fields"), "fields") + fieldItems, err := parseJSONArray(pc, runtime.Str("fields"), "fields") if err != nil { return err } @@ -140,7 +140,7 @@ func executeTableCreate(runtime *common.RuntimeContext) error { result["fields"] = createdFields } if tableIDValue != "" && runtime.Str("view") != "" { - viewItems, err := parseObjectList(fio, runtime.Str("view"), "view") + viewItems, err := parseObjectList(pc, runtime.Str("view"), "view") if err != nil { return err } diff --git a/shortcuts/base/view_ops.go b/shortcuts/base/view_ops.go index 0e8ec3980..7331891b0 100644 --- a/shortcuts/base/view_ops.go +++ b/shortcuts/base/view_ops.go @@ -35,8 +35,9 @@ func dryRunViewGet(_ context.Context, runtime *common.RuntimeContext) *common.Dr } func dryRunViewCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + pc := newParseCtx(runtime) api := dryRunViewBase(runtime) - bodyList, err := parseObjectList(runtime.FileIO(), runtime.Str("json"), "json") + bodyList, err := parseObjectList(pc, runtime.Str("json"), "json") if err != nil || len(bodyList) == 0 { return api.POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/views") } @@ -57,14 +58,16 @@ func dryRunViewGetProperty(runtime *common.RuntimeContext, segment string) *comm } func dryRunViewSetJSONObject(runtime *common.RuntimeContext, segment string) *common.DryRunAPI { - body, _ := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") + pc := newParseCtx(runtime) + body, _ := parseJSONObject(pc, runtime.Str("json"), "json") return dryRunViewBase(runtime). PUT(fmt.Sprintf("/open-apis/base/v3/bases/:base_token/tables/:table_id/views/:view_id/%s", url.PathEscape(segment))). Body(body) } func dryRunViewSetWrapped(runtime *common.RuntimeContext, segment string, wrapper string) *common.DryRunAPI { - raw, err := parseJSONValue(runtime.FileIO(), runtime.Str("json"), "json") + pc := newParseCtx(runtime) + raw, err := parseJSONValue(pc, runtime.Str("json"), "json") if err != nil { raw = nil } @@ -168,9 +171,10 @@ func executeViewGet(runtime *common.RuntimeContext) error { } func executeViewCreate(runtime *common.RuntimeContext) error { + pc := newParseCtx(runtime) baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) - viewItems, err := parseObjectList(runtime.FileIO(), runtime.Str("json"), "json") + viewItems, err := parseObjectList(pc, runtime.Str("json"), "json") if err != nil { return err } @@ -211,10 +215,11 @@ func executeViewGetProperty(runtime *common.RuntimeContext, segment string, key } func executeViewSetJSONObject(runtime *common.RuntimeContext, segment string, key string) error { + pc := newParseCtx(runtime) baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) viewRef := runtime.Str("view-id") - body, err := parseJSONObject(runtime.FileIO(), runtime.Str("json"), "json") + body, err := parseJSONObject(pc, runtime.Str("json"), "json") if err != nil { return err } @@ -227,10 +232,11 @@ func executeViewSetJSONObject(runtime *common.RuntimeContext, segment string, ke } func executeViewSetWrapped(runtime *common.RuntimeContext, segment string, wrapper string, key string) error { + pc := newParseCtx(runtime) baseToken := runtime.Str("base-token") tableIDValue := baseTableID(runtime) viewRef := runtime.Str("view-id") - raw, err := parseJSONValue(runtime.FileIO(), runtime.Str("json"), "json") + raw, err := parseJSONValue(pc, runtime.Str("json"), "json") if err != nil { return err } diff --git a/shortcuts/base/workflow_create.go b/shortcuts/base/workflow_create.go index 0684c4e0c..b4d9b683e 100644 --- a/shortcuts/base/workflow_create.go +++ b/shortcuts/base/workflow_create.go @@ -25,21 +25,21 @@ var BaseWorkflowCreate = common.Shortcut{ if strings.TrimSpace(runtime.Str("base-token")) == "" { return common.FlagErrorf("--base-token must not be blank") } - fio := runtime.FileIO() - raw, err := loadJSONInput(fio, runtime.Str("json"), "json") + pc := newParseCtx(runtime) + raw, err := loadJSONInput(pc, runtime.Str("json"), "json") if err != nil { return err } - if _, err := parseJSONObject(fio, raw, "json"); err != nil { + if _, err := parseJSONObject(pc, raw, "json"); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - fio := runtime.FileIO() + pc := newParseCtx(runtime) var body map[string]interface{} - if raw, err := loadJSONInput(fio, runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(fio, raw, "json") + if raw, err := loadJSONInput(pc, runtime.Str("json"), "json"); err == nil { + body, _ = parseJSONObject(pc, raw, "json") } return common.NewDryRunAPI(). POST("/open-apis/base/v3/bases/:base_token/workflows"). @@ -47,12 +47,12 @@ var BaseWorkflowCreate = common.Shortcut{ Set("base_token", runtime.Str("base-token")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - fio := runtime.FileIO() - raw, err := loadJSONInput(fio, runtime.Str("json"), "json") + pc := newParseCtx(runtime) + raw, err := loadJSONInput(pc, runtime.Str("json"), "json") if err != nil { return err } - body, err := parseJSONObject(fio, raw, "json") + body, err := parseJSONObject(pc, raw, "json") if err != nil { return err } diff --git a/shortcuts/base/workflow_update.go b/shortcuts/base/workflow_update.go index 135090657..09b46f2e7 100644 --- a/shortcuts/base/workflow_update.go +++ b/shortcuts/base/workflow_update.go @@ -29,21 +29,21 @@ var BaseWorkflowUpdate = common.Shortcut{ if strings.TrimSpace(runtime.Str("workflow-id")) == "" { return common.FlagErrorf("--workflow-id must not be blank") } - fio := runtime.FileIO() - raw, err := loadJSONInput(fio, runtime.Str("json"), "json") + pc := newParseCtx(runtime) + raw, err := loadJSONInput(pc, runtime.Str("json"), "json") if err != nil { return err } - if _, err := parseJSONObject(fio, raw, "json"); err != nil { + if _, err := parseJSONObject(pc, raw, "json"); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - fio := runtime.FileIO() + pc := newParseCtx(runtime) var body map[string]interface{} - if raw, err := loadJSONInput(fio, runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(fio, raw, "json") + if raw, err := loadJSONInput(pc, runtime.Str("json"), "json"); err == nil { + body, _ = parseJSONObject(pc, raw, "json") } return common.NewDryRunAPI(). PUT("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id"). @@ -52,12 +52,12 @@ var BaseWorkflowUpdate = common.Shortcut{ Set("workflow_id", runtime.Str("workflow-id")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - fio := runtime.FileIO() - raw, err := loadJSONInput(fio, runtime.Str("json"), "json") + pc := newParseCtx(runtime) + raw, err := loadJSONInput(pc, runtime.Str("json"), "json") if err != nil { return err } - body, err := parseJSONObject(fio, raw, "json") + body, err := parseJSONObject(pc, raw, "json") if err != nil { return err } From 72c772c617e7be317e8f44d36d59dea4782fb1e3 Mon Sep 17 00:00:00 2001 From: tuxedomm <273098272+tuxedomm@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:18:24 +0800 Subject: [PATCH 4/5] fix: improve error messages and remove redundant loadJSONInput calls - Distinguish path validation errors from file access errors in loadJSONInput - Report actual error instead of misleading "file not found" in Stat fallback - Remove redundant loadJSONInput calls in workflow_update (parseJSONObject handles it) Change-Id: I36a0e1471f8cf085737ec0e08ab9aa5113a8a40b --- shortcuts/base/base_shortcut_helpers.go | 7 ++++++- shortcuts/base/record_upload_attachment.go | 2 +- shortcuts/base/workflow_update.go | 16 +++------------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/shortcuts/base/base_shortcut_helpers.go b/shortcuts/base/base_shortcut_helpers.go index 7424392e6..a1d0cb8a5 100644 --- a/shortcuts/base/base_shortcut_helpers.go +++ b/shortcuts/base/base_shortcut_helpers.go @@ -5,6 +5,7 @@ package base import ( "encoding/json" + "errors" "fmt" "io" "strings" @@ -40,7 +41,11 @@ func loadJSONInput(pc *parseCtx, raw string, flagName string) (string, error) { } 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) } defer f.Close() data, err := io.ReadAll(f) diff --git a/shortcuts/base/record_upload_attachment.go b/shortcuts/base/record_upload_attachment.go index fbf3f2456..99ae29977 100644 --- a/shortcuts/base/record_upload_attachment.go +++ b/shortcuts/base/record_upload_attachment.go @@ -95,7 +95,7 @@ func executeRecordUploadAttachment(runtime *common.RuntimeContext) error { if errors.Is(err, fileio.ErrPathValidation) { return output.ErrValidation("unsafe file path: %s", err) } - return output.ErrValidation("file not found: %s", filePath) + return output.ErrValidation("file not accessible: %s: %v", filePath, err) } if fileInfo.Size() > baseAttachmentUploadMaxFileSize { return output.ErrValidation("file %.1fMB exceeds 20MB limit", float64(fileInfo.Size())/1024/1024) diff --git a/shortcuts/base/workflow_update.go b/shortcuts/base/workflow_update.go index 09b46f2e7..2fe2e38cc 100644 --- a/shortcuts/base/workflow_update.go +++ b/shortcuts/base/workflow_update.go @@ -30,11 +30,7 @@ var BaseWorkflowUpdate = common.Shortcut{ return common.FlagErrorf("--workflow-id must not be blank") } pc := newParseCtx(runtime) - raw, err := loadJSONInput(pc, runtime.Str("json"), "json") - if err != nil { - return err - } - if _, err := parseJSONObject(pc, raw, "json"); err != nil { + if _, err := parseJSONObject(pc, runtime.Str("json"), "json"); err != nil { return err } return nil @@ -42,9 +38,7 @@ var BaseWorkflowUpdate = common.Shortcut{ DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { pc := newParseCtx(runtime) var body map[string]interface{} - if raw, err := loadJSONInput(pc, runtime.Str("json"), "json"); err == nil { - body, _ = parseJSONObject(pc, raw, "json") - } + body, _ = parseJSONObject(pc, runtime.Str("json"), "json") return common.NewDryRunAPI(). PUT("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id"). Body(body). @@ -53,11 +47,7 @@ var BaseWorkflowUpdate = common.Shortcut{ }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { pc := newParseCtx(runtime) - raw, err := loadJSONInput(pc, runtime.Str("json"), "json") - if err != nil { - return err - } - body, err := parseJSONObject(pc, raw, "json") + body, err := parseJSONObject(pc, runtime.Str("json"), "json") if err != nil { return err } From 0684f380098f9bf6406a16f64b61d640c03df8dd Mon Sep 17 00:00:00 2001 From: tuxedomm <273098272+tuxedomm@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:45:16 +0800 Subject: [PATCH 5/5] fix: guard nil FileIO before @file and attachment operations RuntimeContext.FileIO() can return nil when no provider is registered. Add nil checks before Open/Stat calls to return structured errors instead of panicking. Change-Id: Ie00799d0dea559411d9429f3a88acd02428a47bf --- shortcuts/base/base_shortcut_helpers.go | 3 +++ shortcuts/base/record_upload_attachment.go | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/shortcuts/base/base_shortcut_helpers.go b/shortcuts/base/base_shortcut_helpers.go index a1d0cb8a5..925c0b300 100644 --- a/shortcuts/base/base_shortcut_helpers.go +++ b/shortcuts/base/base_shortcut_helpers.go @@ -39,6 +39,9 @@ func loadJSONInput(pc *parseCtx, raw string, flagName string) (string, error) { if path == "" { return "", common.FlagErrorf("--%s file path cannot be empty after @", flagName) } + if pc.fio == nil { + return "", common.FlagErrorf("--%s @file inputs require a FileIO provider", flagName) + } f, err := pc.fio.Open(path) if err != nil { var pathErr *fileio.PathValidationError diff --git a/shortcuts/base/record_upload_attachment.go b/shortcuts/base/record_upload_attachment.go index 99ae29977..b744327e2 100644 --- a/shortcuts/base/record_upload_attachment.go +++ b/shortcuts/base/record_upload_attachment.go @@ -90,7 +90,11 @@ func dryRunRecordUploadAttachment(_ context.Context, runtime *common.RuntimeCont func executeRecordUploadAttachment(runtime *common.RuntimeContext) error { filePath := runtime.Str("file") - fileInfo, err := runtime.FileIO().Stat(filePath) + fio := runtime.FileIO() + if fio == nil { + return output.ErrValidation("file operations require a FileIO provider") + } + fileInfo, err := fio.Stat(filePath) if err != nil { if errors.Is(err, fileio.ErrPathValidation) { return output.ErrValidation("unsafe file path: %s", err)