mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
255 lines
7.6 KiB
Go
255 lines
7.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package base
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/extension/fileio"
|
|
"github.com/larksuite/cli/internal/errclass"
|
|
"github.com/larksuite/cli/internal/util"
|
|
)
|
|
|
|
func handleBaseAPIResult(result interface{}, err error, action string) (map[string]interface{}, error) {
|
|
data, err := handleBaseAPIResultAny(result, err, action)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dataMap, _ := data.(map[string]interface{})
|
|
return dataMap, nil
|
|
}
|
|
|
|
// handleBaseAPIResultAny normalizes the Base v3 {code,msg,data} envelope used
|
|
// by shortcut APIs. Success returns data as-is; API failures become the CLI's
|
|
// structured ErrAPI, with server-provided message/hint promoted to the top level.
|
|
func handleBaseAPIResultAny(result interface{}, err error, action string) (interface{}, error) {
|
|
if err != nil {
|
|
return nil, baseAPIBoundaryError(err, action)
|
|
}
|
|
|
|
resultMap, ok := result.(map[string]interface{})
|
|
if !ok || resultMap == nil {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "%s: API returned a malformed response envelope", action)
|
|
}
|
|
if _, exists := resultMap["code"]; !exists {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "%s: API response is missing code", action)
|
|
}
|
|
code, numeric := util.ToFloat64(resultMap["code"])
|
|
if !numeric {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "%s: API response code is not numeric", action)
|
|
}
|
|
if code == 0 {
|
|
return resultMap["data"], nil
|
|
}
|
|
|
|
return nil, baseAPIErrorFromResult(resultMap, errclass.ClassifyContext{})
|
|
}
|
|
|
|
// baseFlagErrorf marks flag-usage failures; it shares baseValidationErrorf's
|
|
// typed envelope and exists so call sites read as flag rejections.
|
|
func baseFlagErrorf(format string, args ...any) error {
|
|
return baseValidationErrorf(format, args...)
|
|
}
|
|
|
|
func baseValidationErrorf(format string, args ...any) error {
|
|
msg := fmt.Sprintf(format, args...)
|
|
err := errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", msg)
|
|
if params := flagParams(msg); len(params) > 0 {
|
|
err = err.WithParam(params[0].Name).WithParams(params...)
|
|
}
|
|
if cause := firstErrorArg(args); cause != nil {
|
|
err = err.WithCause(cause)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func flagParams(msg string) []errs.InvalidParam {
|
|
reason := msg
|
|
seen := map[string]bool{}
|
|
params := []errs.InvalidParam{}
|
|
for start := strings.Index(msg, "--"); start >= 0; start = strings.Index(msg, "--") {
|
|
end := start + 2
|
|
for end < len(msg) {
|
|
ch := msg[end]
|
|
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' {
|
|
end++
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if end > start+2 {
|
|
name := msg[start:end]
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
params = append(params, errs.InvalidParam{Name: name, Reason: reason})
|
|
}
|
|
}
|
|
msg = msg[end:]
|
|
}
|
|
return params
|
|
}
|
|
|
|
func firstErrorArg(args []any) error {
|
|
for _, arg := range args {
|
|
if err, ok := arg.(error); ok {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// baseMissingFileIOError reports a broken runtime wiring: a command that needs
|
|
// local file access was constructed without a FileIO provider. The user cannot
|
|
// fix this by changing flags, so it classifies as internal, not validation.
|
|
func baseMissingFileIOError(format string, args ...any) error {
|
|
return errs.NewInternalError(errs.SubtypeFileIO, format, args...)
|
|
}
|
|
|
|
func baseInputStatError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, fileio.ErrPathValidation) {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe file path: %s", err).WithCause(err)
|
|
}
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot read file: %s", err).WithCause(err)
|
|
}
|
|
|
|
func baseSaveError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var me *fileio.MkdirError
|
|
switch {
|
|
case errors.Is(err, fileio.ErrPathValidation):
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithCause(err)
|
|
case errors.As(err, &me):
|
|
return errs.NewInternalError(errs.SubtypeFileIO, "cannot create parent directory: %s", err).WithCause(err)
|
|
default:
|
|
return errs.NewInternalError(errs.SubtypeFileIO, "cannot create file: %s", err).WithCause(err)
|
|
}
|
|
}
|
|
|
|
func baseAPIBoundaryError(err error, action string) error {
|
|
if _, ok := errs.ProblemOf(err); ok {
|
|
return err
|
|
}
|
|
return errs.NewNetworkError(errs.SubtypeNetworkTransport, "%s: %s", action, err).WithCause(err)
|
|
}
|
|
|
|
func baseUploadAttachmentError(filePath string, err error) error {
|
|
if p, ok := errs.ProblemOf(err); ok {
|
|
p.Message = fmt.Sprintf("failed to upload attachment %s: %s", filePath, p.Message)
|
|
return err
|
|
}
|
|
return errs.NewInternalError(errs.SubtypeSDKError, "failed to upload attachment %s: %s", filePath, err).WithCause(err)
|
|
}
|
|
|
|
func baseAPIErrorFromResult(resultMap map[string]interface{}, cc errclass.ClassifyContext) error {
|
|
if resultMap == nil {
|
|
return errs.NewInternalError(errs.SubtypeInvalidResponse, "API returned a malformed response envelope")
|
|
}
|
|
if msg := extractDataErrorMessage(resultMap); msg != "" {
|
|
resultMap["msg"] = msg
|
|
}
|
|
hint := extractErrorHint(resultMap)
|
|
if logID := extractBaseErrorLogID(resultMap); logID != "" {
|
|
resultMap["log_id"] = logID
|
|
}
|
|
err := errclass.BuildAPIError(resultMap, cc)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if p, ok := errs.ProblemOf(err); ok && hint != "" {
|
|
p.Hint = hint
|
|
}
|
|
return err
|
|
}
|
|
|
|
func enrichBaseAPIErrorFromBody(err error, body []byte, cc errclass.ClassifyContext) error {
|
|
if _, ok := errs.ProblemOf(err); !ok {
|
|
return err
|
|
}
|
|
result, parseErr := decodeBaseV3Response(body)
|
|
if parseErr != nil {
|
|
return err
|
|
}
|
|
enriched := baseAPIErrorFromResult(result, cc)
|
|
if enriched == nil {
|
|
return err
|
|
}
|
|
src, _ := errs.ProblemOf(enriched)
|
|
dst, _ := errs.ProblemOf(err)
|
|
if src != nil && dst != nil {
|
|
dst.Message = src.Message
|
|
dst.Hint = src.Hint
|
|
// A body without log_id must not erase a header-derived LogID
|
|
// already carried by err.
|
|
if src.LogID != "" {
|
|
dst.LogID = src.LogID
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func extractBaseErrorLogID(resultMap map[string]interface{}) string {
|
|
for _, key := range []string{"log_id", "logid"} {
|
|
if logID, _ := resultMap[key].(string); strings.TrimSpace(logID) != "" {
|
|
return strings.TrimSpace(logID)
|
|
}
|
|
}
|
|
if detail, ok := resultMap["error"].(map[string]interface{}); ok {
|
|
for _, key := range []string{"log_id", "logid"} {
|
|
if logID, _ := detail[key].(string); strings.TrimSpace(logID) != "" {
|
|
return strings.TrimSpace(logID)
|
|
}
|
|
}
|
|
}
|
|
data, _ := resultMap["data"].(map[string]interface{})
|
|
if detail, ok := data["error"].(map[string]interface{}); ok {
|
|
for _, key := range []string{"log_id", "logid"} {
|
|
if logID, _ := detail[key].(string); strings.TrimSpace(logID) != "" {
|
|
return strings.TrimSpace(logID)
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func extractErrorHint(resultMap map[string]interface{}) string {
|
|
if detail, ok := resultMap["error"].(map[string]interface{}); ok {
|
|
if hint := consumeStringField(detail, "hint"); hint != "" {
|
|
return hint
|
|
}
|
|
}
|
|
data, _ := resultMap["data"].(map[string]interface{})
|
|
if detail, ok := data["error"].(map[string]interface{}); ok {
|
|
if hint := consumeStringField(detail, "hint"); hint != "" {
|
|
return hint
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func extractDataErrorMessage(resultMap map[string]interface{}) string {
|
|
data, _ := resultMap["data"].(map[string]interface{})
|
|
if detail, ok := data["error"].(map[string]interface{}); ok {
|
|
if message := consumeStringField(detail, "message"); message != "" {
|
|
return message
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func consumeStringField(src map[string]interface{}, key string) string {
|
|
value, _ := src[key].(string)
|
|
if _, exists := src[key]; exists {
|
|
delete(src, key)
|
|
}
|
|
return strings.TrimSpace(value)
|
|
}
|