Skip to content
Open
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
6 changes: 3 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,20 @@ linters:
- forbidigo
# errs-typed-only enforced on paths already migrated to errs.NewXxxError.
# Add a path when its migration is complete.
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/calendar/helpers\.go|shortcuts/drive/)
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/calendar/helpers\.go|shortcuts/drive/|shortcuts/im/)
text: errs-typed-only
linters:
- forbidigo
# errs-no-bare-wrap enforced on paths fully migrated to typed final
# errors. Scoped separately from errs-typed-only because cmd/auth/,
# cmd/config/ still have residual fmt.Errorf and must not be caught.
- path-except: (shortcuts/drive/|shortcuts/calendar/helpers\.go|shortcuts/common/mcp_client\.go)
- path-except: (shortcuts/drive/|shortcuts/im/|shortcuts/calendar/helpers\.go|shortcuts/common/mcp_client\.go)
text: errs-no-bare-wrap
linters:
- forbidigo
# errs-no-legacy-helper is drive-only: the shared helpers it bans are
# still used by other domains until their later migration phase.
- path-except: (shortcuts/drive/)
- path-except: (shortcuts/drive/|shortcuts/im/)
text: errs-no-legacy-helper
linters:
- forbidigo
Expand Down
1 change: 1 addition & 0 deletions lint/errscontract/rule_no_legacy_envelope_literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
// appending their path prefix here.
var migratedEnvelopePaths = []string{
"shortcuts/drive/",
"shortcuts/im/",
}

// legacyOutputImportPath is the import path of the package that declares the
Expand Down
39 changes: 39 additions & 0 deletions shortcuts/common/call_api_typed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,42 @@ func TestCallAPITyped_NonObjectJSON(t *testing.T) {
t.Errorf("subtype = %q, want %q", intErr.Subtype, errs.SubtypeInvalidResponse)
}
}

// TestDoAPIJSONTyped_Success returns the data object on code 0, confirming the
// typed DoAPIJSON replacement preserves the success contract of DoAPIJSON.
func TestDoAPIJSONTyped_Success(t *testing.T) {
rt, reg := newCallAPITypedRuntime(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/x/z",
Body: map[string]interface{}{"code": float64(0), "data": map[string]interface{}{"id": "z1"}},
})

data, err := rt.DoAPIJSONTyped("GET", "/open-apis/x/z", nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if data["id"] != "z1" {
t.Errorf("data[id] = %v, want z1", data["id"])
}
}

// TestDoAPIJSONTyped_NonZeroCode classifies a non-zero API code into a typed
// errs.* error (carrying log_id), never a legacy output.ExitError envelope.
func TestDoAPIJSONTyped_NonZeroCode(t *testing.T) {
rt, reg := newCallAPITypedRuntime(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/x/z",
Body: map[string]interface{}{"code": float64(1061044), "msg": "boom", "log_id": "lz"},
})

_, err := rt.DoAPIJSONTyped("POST", "/open-apis/x/z", nil, map[string]any{})
p, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected a typed errs.* error, got %T: %v", err, err)
}
if p.LogID != "lz" {
t.Errorf("LogID = %q, want lz", p.LogID)
}
}
22 changes: 22 additions & 0 deletions shortcuts/common/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,28 @@ func (ctx *RuntimeContext) DoAPIJSONWithLogID(method, apiPath string, query lark
return ctx.doAPIJSON(method, apiPath, query, body, true)
}

// DoAPIJSONTyped is the typed-only replacement for DoAPIJSON: it issues the same
// larkcore.ApiReq request (identical method / path / query / body model) but
// classifies failures into typed errs.* errors via ClassifyAPIResponse instead
// of emitting a legacy output.ExitError "api_error" envelope. A transport / auth
// error from the client boundary is already typed and passes through unchanged;
// a non-zero API code is classified with subtype / code / log_id.
func (ctx *RuntimeContext) DoAPIJSONTyped(method, apiPath string, query larkcore.QueryParams, body any) (map[string]any, error) {
req := &larkcore.ApiReq{
HttpMethod: method,
ApiPath: apiPath,
QueryParams: query,
}
if body != nil {
req.Body = body
}
resp, err := ctx.DoAPI(req)
if err != nil {
return nil, err
}
return ctx.ClassifyAPIResponse(resp)
}

func (ctx *RuntimeContext) doAPIJSON(method, apiPath string, query larkcore.QueryParams, body any, includeLogID bool) (map[string]any, error) {
req := &larkcore.ApiReq{
HttpMethod: method,
Expand Down
18 changes: 9 additions & 9 deletions shortcuts/im/convert_lib/card_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,15 @@ func TestConvertAtWithMentions(t *testing.T) {
mentions := []interface{}{
map[string]interface{}{
"key": "@_user_1",
"id": "ou_6b64bef911a5a3ea763df8ffd9258f59",
"name": "燕忠毅",
"id": "ou_xxxx",
"name": "张三",
},
}
attachment := cardObj{
"at_users": cardObj{
"cde8a6c8": cardObj{
"user_id": "754700000001",
"content": "燕忠毅",
"user_id": "1234567890",
"content": "张三",
"mention_key": "@_user_1",
},
},
Expand All @@ -267,7 +267,7 @@ func TestConvertAtWithMentions(t *testing.T) {
attachment: attachment,
mentionsByKey: buildMentionsByKey(mentions),
}
if got := concise.convertAt(cardObj{"userID": "cde8a6c8"}); got != "@燕忠毅(ou_6b64bef911a5a3ea763df8ffd9258f59)" {
if got := concise.convertAt(cardObj{"userID": "cde8a6c8"}); got != "@张三(ou_xxxx)" {
t.Fatalf("convertAt(concise with mentions) = %q", got)
}

Expand All @@ -277,7 +277,7 @@ func TestConvertAtWithMentions(t *testing.T) {
attachment: attachment,
mentionsByKey: buildMentionsByKey(mentions),
}
if got := detailed.convertAt(cardObj{"userID": "cde8a6c8"}); got != "@燕忠毅(open_id:ou_6b64bef911a5a3ea763df8ffd9258f59)" {
if got := detailed.convertAt(cardObj{"userID": "cde8a6c8"}); got != "@张三(open_id:ou_xxxx)" {
t.Fatalf("convertAt(detailed with mentions) = %q", got)
}

Expand All @@ -300,14 +300,14 @@ func TestConvertAtWithMentions(t *testing.T) {
attachment: cardObj{
"at_users": cardObj{
"cde8a6c8": cardObj{
"user_id": "754700000001",
"content": "燕忠毅",
"user_id": "1234567890",
"content": "张三",
"mention_key": "@_user_1",
},
},
},
}
if got := nilMentions.convertAt(cardObj{"userID": "cde8a6c8"}); got != "@燕忠毅(user_id:754700000001)" {
if got := nilMentions.convertAt(cardObj{"userID": "cde8a6c8"}); got != "@张三(user_id:1234567890)" {
t.Fatalf("convertAt(fallback nil mentionsByKey) = %q", got)
}
}
Expand Down
4 changes: 2 additions & 2 deletions shortcuts/im/convert_lib/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func batchResolveByBasicContact(runtime *common.RuntimeContext, missingIDs []str
}
batch := missingIDs[i:end]

data, err := runtime.DoAPIJSON(http.MethodPost,
data, err := runtime.DoAPIJSONTyped(http.MethodPost,
"/open-apis/contact/v3/users/basic_batch",
larkcore.QueryParams{"user_id_type": []string{"open_id"}},
map[string]interface{}{"user_ids": batch},
Expand Down Expand Up @@ -198,7 +198,7 @@ func batchResolveUsers(runtime *common.RuntimeContext, missingIDs []string, name
}
apiURL := "/open-apis/contact/v3/users/batch?" + strings.Join(parts, "&")

data, err := runtime.DoAPIJSON(http.MethodGet, apiURL, nil, nil)
data, err := runtime.DoAPIJSONTyped(http.MethodGet, apiURL, nil, nil)
if err != nil {
break
}
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/im/convert_lib/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func batchResolveMergeForwardSenders(runtime *common.RuntimeContext, prefetch ma
// "code: 2200 Internal Error" with its log_id would show up as just "empty
// data" in the output).
func fetchMergeForwardSubMessages(messageID string, runtime *common.RuntimeContext) ([]map[string]interface{}, error) {
data, err := runtime.DoAPIJSON(http.MethodGet, mergeForwardMessagesPath(messageID), larkcore.QueryParams{
data, err := runtime.DoAPIJSONTyped(http.MethodGet, mergeForwardMessagesPath(messageID), larkcore.QueryParams{
"user_id_type": []string{"open_id"},
"card_msg_content_type": []string{"raw_card_content"},
}, nil)
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/im/convert_lib/reactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func fetchReactionsBatch(runtime *common.RuntimeContext, batchIDs []string, idIn
queries = append(queries, map[string]interface{}{"message_id": id})
}

data, err := runtime.DoAPIJSON(http.MethodPost,
data, err := runtime.DoAPIJSONTyped(http.MethodPost,
"/open-apis/im/v1/messages/reactions/batch_query",
nil,
map[string]interface{}{"queries": queries},
Expand Down
4 changes: 2 additions & 2 deletions shortcuts/im/convert_lib/thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,15 @@ func ExpandThreadReplies(runtime *common.RuntimeContext, messages []map[string]i
// Returns the raw message items, whether more replies exist beyond the limit,
// and a non-nil error when the API call fails.
func fetchThreadReplies(runtime *common.RuntimeContext, threadID string, limit int) ([]map[string]interface{}, bool, error) {
data, err := runtime.DoAPIJSON(http.MethodGet, "/open-apis/im/v1/messages", larkcore.QueryParams{
data, err := runtime.DoAPIJSONTyped(http.MethodGet, "/open-apis/im/v1/messages", larkcore.QueryParams{
"container_id_type": []string{"thread"},
"container_id": []string{threadID},
"sort_type": []string{"ByCreateTimeAsc"},
"page_size": []string{fmt.Sprint(limit)},
"card_msg_content_type": []string{"raw_card_content"},
}, nil)
if err != nil {
return nil, false, fmt.Errorf("fetch thread replies for %s: %w", threadID, err)
return nil, false, fmt.Errorf("fetch thread replies for %s: %w", threadID, err) //nolint:forbidigo // best-effort internal thread fetch; never surfaced as a final shortcut error (ExpandThreadReplies is void)
}
hasMore, _ := data["has_more"].(bool)
rawItems, _ := data["items"].([]interface{})
Expand Down
Loading
Loading