From c4c0438015798b18f1745a69167fde4343f89907 Mon Sep 17 00:00:00 2001 From: "zhaoyukun.yk" Date: Tue, 2 Jun 2026 22:48:21 +0800 Subject: [PATCH] feat(calendar): typed error envelopes for validation/internal errors Calendar commands now surface validation and internal errors as structured, typed error envelopes with consistent exit codes and a machine-readable shape, so callers (and AI agents) can reliably tell bad input apart from internal faults. API-response error paths keep the existing envelope for now and will move to the typed shape together with the typed API-call path. --- shortcuts/calendar/calendar_agenda.go | 15 +- shortcuts/calendar/calendar_create.go | 31 ++- shortcuts/calendar/calendar_freebusy.go | 20 +- shortcuts/calendar/calendar_room_find.go | 43 +-- shortcuts/calendar/calendar_rsvp.go | 7 +- shortcuts/calendar/calendar_suggestion.go | 47 ++-- shortcuts/calendar/calendar_test.go | 317 ++++++++++++++++++++++ shortcuts/calendar/calendar_update.go | 32 ++- 8 files changed, 428 insertions(+), 84 deletions(-) diff --git a/shortcuts/calendar/calendar_agenda.go b/shortcuts/calendar/calendar_agenda.go index c972bdb9e..3191eed29 100644 --- a/shortcuts/calendar/calendar_agenda.go +++ b/shortcuts/calendar/calendar_agenda.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/util" "github.com/larksuite/cli/internal/validate" @@ -29,7 +30,7 @@ const ( func fetchInstanceViewRange(ctx context.Context, runtime *common.RuntimeContext, calendarId string, startTime, endTime int64, depth int) ([]map[string]interface{}, error) { if depth > 10 { - return nil, output.Errorf(output.ExitInternal, "recursion_limit", "too many splits for instance_view") + return nil, errs.NewInternalError(errs.SubtypeUnknown, "too many splits for instance_view") } if startTime > endTime { return nil, nil @@ -54,6 +55,7 @@ func fetchInstanceViewRange(ctx context.Context, runtime *common.RuntimeContext, "start_time": fmt.Sprintf("%d", startTime), "end_time": fmt.Sprintf("%d", endTime), }, nil) + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). err = wrapPredefinedError(err) if err != nil { return nil, output.Errorf(output.ExitAPI, "api_error", "API call failed: %s", err) @@ -78,6 +80,7 @@ func fetchInstanceViewRange(ctx context.Context, runtime *common.RuntimeContext, if int(code) == larkErrCalendarTimeRangeExceeded { mid := startTime + span/2 if mid <= startTime { + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). return nil, output.Errorf(output.ExitAPI, "api_error", "query failed: time range exceeds 40-day limit, please narrow the range") } left, err := fetchInstanceViewRange(ctx, runtime, calendarId, startTime, mid, depth+1) @@ -94,6 +97,7 @@ func fetchInstanceViewRange(ctx context.Context, runtime *common.RuntimeContext, // Error 193104: too many instances -> split if int(code) == larkErrCalendarTooManyInstances { if span <= minSplitWindowSeconds { + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). return nil, output.Errorf(output.ExitAPI, "api_error", "query failed: more than 1000 instances in the time range, please narrow the range") } mid := startTime + span/2 @@ -109,6 +113,7 @@ func fetchInstanceViewRange(ctx context.Context, runtime *common.RuntimeContext, } msg, _ := resultMap["msg"].(string) + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). return nil, output.ErrAPI(int(code), msg, resultMap["error"]) } @@ -147,20 +152,20 @@ func parseTimeRange(runtime *common.RuntimeContext) (int64, int64, error) { startTime, err := common.ParseTime(startInput) if err != nil { - return 0, 0, output.ErrValidation("--start: %v", err) + return 0, 0, errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTime, err := common.ParseTime(endInput, "end") if err != nil { - return 0, 0, output.ErrValidation("--end: %v", err) + return 0, 0, errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } startInt, err := strconv.ParseInt(startTime, 10, 64) if err != nil { - return 0, 0, output.ErrValidation("invalid start time: %v", err) + return 0, 0, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start") } endInt, err := strconv.ParseInt(endTime, 10, 64) if err != nil { - return 0, 0, output.ErrValidation("invalid end time: %v", err) + return 0, 0, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end") } return startInt, endInt, nil diff --git a/shortcuts/calendar/calendar_create.go b/shortcuts/calendar/calendar_create.go index 6483a2f7f..ab59e85fe 100644 --- a/shortcuts/calendar/calendar_create.go +++ b/shortcuts/calendar/calendar_create.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/shortcuts/common" @@ -60,7 +61,7 @@ func parseAttendees(attendeesStr string, currentUserId string) ([]map[string]str case strings.HasPrefix(id, "ou_"): attendees = append(attendees, map[string]string{"type": "user", "user_id": id}) default: - return nil, fmt.Errorf("unsupported attendee id format: %s", id) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "unsupported attendee id format: %s", id) } } return attendees, nil @@ -90,7 +91,7 @@ var CalendarCreate = common.Shortcut{ for _, flag := range []string{"summary", "description", "rrule", "calendar-id"} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { - return output.ErrValidation(err.Error()) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err) } } } @@ -102,35 +103,35 @@ var CalendarCreate = common.Shortcut{ continue } if !strings.HasPrefix(id, "ou_") && !strings.HasPrefix(id, "oc_") && !strings.HasPrefix(id, "omm_") { - return output.ErrValidation("invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id).WithParam("--attendee-ids") } } } if runtime.Str("start") == "" { - return common.FlagErrorf("specify --start (e.g. '2026-03-12T14:00+08:00')") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --start (e.g. '2026-03-12T14:00+08:00')").WithParam("--start") } if runtime.Str("end") == "" { - return common.FlagErrorf("specify --end (e.g. '2026-03-12T15:00+08:00')") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --end (e.g. '2026-03-12T15:00+08:00')").WithParam("--end") } startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { - return common.FlagErrorf("--start: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { - return common.FlagErrorf("--end: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } s, err := strconv.ParseInt(startTs, 10, 64) if err != nil { - return common.FlagErrorf("invalid start time: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start") } e, err := strconv.ParseInt(endTs, 10, 64) if err != nil { - return common.FlagErrorf("invalid end time: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end") } if e <= s { - return common.FlagErrorf("end time must be after start time") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "end time must be after start time") } return nil }, @@ -183,16 +184,17 @@ var CalendarCreate = common.Shortcut{ startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { - return output.ErrValidation("--start: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { - return output.ErrValidation("--end: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } eventData := buildEventData(runtime, startTs, endTs) // Create event + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). data, err := runtime.CallAPI("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events", validate.EncodePathSegment(calendarId)), nil, eventData) @@ -203,7 +205,7 @@ var CalendarCreate = common.Shortcut{ event, _ := data["event"].(map[string]interface{}) eventId, _ := event["event_id"].(string) if eventId == "" { - return output.Errorf(output.ExitAPI, "api_error", "failed to create event: no event_id returned") + return errs.NewInternalError(errs.SubtypeInvalidResponse, "failed to create event: no event_id returned") } // Add attendees if specified @@ -214,9 +216,10 @@ var CalendarCreate = common.Shortcut{ } attendees, err := parseAttendees(attendeesStr, currentUserId) if err != nil { - return output.ErrValidation("invalid attendee id: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id: %v", err) } + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). _, err = runtime.CallAPI("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s/attendees", validate.EncodePathSegment(calendarId), validate.EncodePathSegment(eventId)), map[string]interface{}{"user_id_type": "open_id"}, diff --git a/shortcuts/calendar/calendar_freebusy.go b/shortcuts/calendar/calendar_freebusy.go index 1de1a67e2..21399f64a 100644 --- a/shortcuts/calendar/calendar_freebusy.go +++ b/shortcuts/calendar/calendar_freebusy.go @@ -10,6 +10,7 @@ import ( "strconv" "time" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" ) @@ -20,20 +21,20 @@ func parseFreebusyTimeRange(runtime *common.RuntimeContext) (string, string, err startTs, err := common.ParseTime(startInput) if err != nil { - return "", "", output.ErrValidation("--start: %v", err) + return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTs, err := common.ParseTime(endInput, "end") if err != nil { - return "", "", output.ErrValidation("--end: %v", err) + return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } startSec, err := strconv.ParseInt(startTs, 10, 64) if err != nil { - return "", "", output.ErrValidation("invalid start timestamp: %v", err) + return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start timestamp: %v", err) } endSec, err := strconv.ParseInt(endTs, 10, 64) if err != nil { - return "", "", output.ErrValidation("invalid end timestamp: %v", err) + return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end timestamp: %v", err) } timeMin := time.Unix(startSec, 0).Format(time.RFC3339) @@ -73,14 +74,14 @@ var CalendarFreebusy = common.Shortcut{ } userId := runtime.Str("user-id") if userId == "" && runtime.IsBot() { - return common.FlagErrorf("--user-id is required for bot identity") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id is required for bot identity").WithParam("--user-id") } if userId == "" && runtime.UserOpenId() == "" { - return common.FlagErrorf("cannot determine user ID, specify --user-id or ensure you are logged in") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "cannot determine user ID, specify --user-id or ensure you are logged in").WithParam("--user-id") } if userId != "" { if _, err := common.ValidateUserID(userId); err != nil { - return err + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--user-id") } } return nil @@ -93,9 +94,12 @@ var CalendarFreebusy = common.Shortcut{ timeMin, timeMax, err := parseFreebusyTimeRange(runtime) if err != nil { - return output.ErrValidation("--start/--end: %v", err) + // parseFreebusyTimeRange already returns a typed *errs.ValidationError + // carrying the offending flag in .Param; pass it through unchanged. + return err } + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). data, err := runtime.CallAPI("POST", "/open-apis/calendar/v4/freebusy/list", nil, map[string]interface{}{ "time_min": timeMin, "time_max": timeMax, diff --git a/shortcuts/calendar/calendar_room_find.go b/shortcuts/calendar/calendar_room_find.go index 743942e94..9e7b3075c 100644 --- a/shortcuts/calendar/calendar_room_find.go +++ b/shortcuts/calendar/calendar_room_find.go @@ -15,6 +15,8 @@ import ( "sync" "time" + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/internal/errclass" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" larkcore "github.com/larksuite/oapi-sdk-go/v3/core" @@ -126,40 +128,40 @@ func collectRoomFindResults(slots []roomFindSlot, limit int, fetch func(roomFind func parseRoomFindSlots(runtime *common.RuntimeContext) ([]roomFindSlot, error) { rawSlots := runtime.StrArray(flagSlot) if len(rawSlots) == 0 { - return nil, output.ErrValidation("specify at least one --slot") + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "specify at least one --slot").WithParam("--slot") } slots := make([]roomFindSlot, 0, len(rawSlots)) for _, raw := range rawSlots { parts := strings.Split(strings.TrimSpace(raw), "~") if len(parts) != 2 { - return nil, output.ErrValidation("invalid --slot format %q, expected start~end", raw) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --slot format %q, expected start~end", raw).WithParam("--slot") } startTs, err := common.ParseTime(parts[0]) if err != nil { - return nil, output.ErrValidation("invalid slot start time %q: %v", parts[0], err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid slot start time %q: %v", parts[0], err).WithParam("--slot") } endTs, err := common.ParseTime(parts[1]) if err != nil { - return nil, output.ErrValidation("invalid slot end time %q: %v", parts[1], err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid slot end time %q: %v", parts[1], err).WithParam("--slot") } startSec, err := strconv.ParseInt(startTs, 10, 64) if err != nil { - return nil, output.ErrValidation("invalid slot start timestamp %q: %v", startTs, err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid slot start timestamp %q: %v", startTs, err).WithParam("--slot") } endSec, err := strconv.ParseInt(endTs, 10, 64) if err != nil { - return nil, output.ErrValidation("invalid slot end timestamp %q: %v", endTs, err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid slot end timestamp %q: %v", endTs, err).WithParam("--slot") } if endSec <= startSec { - return nil, output.ErrValidation("--slot end time must be after start time: %q", raw) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--slot end time must be after start time: %q", raw).WithParam("--slot") } startRFC3339, err := unixStringToRFC3339(startTs) if err != nil { - return nil, output.ErrValidation("invalid slot start timestamp %q: %v", startTs, err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid slot start timestamp %q: %v", startTs, err).WithParam("--slot") } endRFC3339, err := unixStringToRFC3339(endTs) if err != nil { - return nil, output.ErrValidation("invalid slot end timestamp %q: %v", endTs, err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid slot end timestamp %q: %v", endTs, err).WithParam("--slot") } slots = append(slots, roomFindSlot{Start: startRFC3339, End: endRFC3339}) } @@ -196,7 +198,7 @@ func parseRoomFindAttendees(attendeesStr string, currentUserID string) ([]string seenChats[id] = true } default: - return nil, nil, output.ErrValidation("invalid attendee id format %q: should start with 'ou_' or 'oc_'", id) + return nil, nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_' or 'oc_'", id) } } if currentUserID != "" && !seenUsers[currentUserID] { @@ -249,20 +251,23 @@ func callRoomFind(runtime *common.RuntimeContext, req *roomFindRequest) ([]*room Body: req, }) if err != nil { - return nil, err + if _, ok := errs.ProblemOf(err); ok { + return nil, err + } + return nil, errs.WrapInternal(err) } if apiResp.StatusCode < http.StatusOK || apiResp.StatusCode >= http.StatusMultipleChoices { - return nil, output.ErrAPI(apiResp.StatusCode, "", string(apiResp.RawBody)) + return nil, errs.NewAPIError(errs.SubtypeUnknown, "%s", string(apiResp.RawBody)).WithCode(apiResp.StatusCode) } var resp = &OpenAPIResponse[*roomFindData]{} if err := json.Unmarshal(apiResp.RawBody, &resp); err != nil { - return nil, output.ErrWithHint(output.ExitInternal, "validation", "unmarshal response fail", err.Error()) + return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "unmarshal response fail").WithCause(err) } if resp.Code != 0 { - return nil, output.ErrAPI(resp.Code, resp.Msg, resp.Data) + return nil, errclass.BuildAPIError(map[string]any{"code": resp.Code, "msg": resp.Msg}, errclass.ClassifyContext{}) } if resp.Data != nil { @@ -318,7 +323,7 @@ var CalendarRoomFind = common.Shortcut{ for _, flag := range []string{flagCity, flagBuilding, flagFloor, flagEventRrule, flagTimezone} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { - return output.ErrValidation(err.Error()) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err) } } } @@ -328,7 +333,7 @@ var CalendarRoomFind = common.Shortcut{ continue } if err := common.RejectDangerousChars("--"+flagRoomName, name); err != nil { - return output.ErrValidation(err.Error()) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err) } } if _, err := parseRoomFindSlots(runtime); err != nil { @@ -338,13 +343,13 @@ var CalendarRoomFind = common.Shortcut{ return err } if minCapacity := runtime.Int(flagMinCapacity); minCapacity < 0 { - return output.ErrValidation("--min-capacity must be >= 0") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--min-capacity must be >= 0").WithParam("--min-capacity") } if maxCapacity := runtime.Int(flagMaxCapacity); maxCapacity < 0 { - return output.ErrValidation("--max-capacity must be >= 0") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--max-capacity must be >= 0").WithParam("--max-capacity") } if minCapacity, maxCapacity := runtime.Int(flagMinCapacity), runtime.Int(flagMaxCapacity); minCapacity > 0 && maxCapacity > 0 && minCapacity > maxCapacity { - return output.ErrValidation("--min-capacity must be <= --max-capacity") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--min-capacity must be <= --max-capacity").WithParam("--min-capacity") } return nil }, diff --git a/shortcuts/calendar/calendar_rsvp.go b/shortcuts/calendar/calendar_rsvp.go index ef0dd35f6..f9a1122fd 100644 --- a/shortcuts/calendar/calendar_rsvp.go +++ b/shortcuts/calendar/calendar_rsvp.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/shortcuts/common" ) @@ -52,14 +52,14 @@ var CalendarRsvp = common.Shortcut{ for _, flag := range []string{"calendar-id", "event-id", "rsvp-status"} { if val := strings.TrimSpace(runtime.Str(flag)); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { - return output.ErrValidation(err.Error()) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err) } } } eventId := strings.TrimSpace(runtime.Str("event-id")) if eventId == "" { - return output.ErrValidation("event-id cannot be empty") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "event-id cannot be empty").WithParam("--event-id") } return nil }, @@ -71,6 +71,7 @@ var CalendarRsvp = common.Shortcut{ eventId := strings.TrimSpace(runtime.Str("event-id")) status := strings.TrimSpace(runtime.Str("rsvp-status")) + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). _, err := runtime.DoAPIJSON("POST", fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s/reply", validate.EncodePathSegment(calendarId), diff --git a/shortcuts/calendar/calendar_suggestion.go b/shortcuts/calendar/calendar_suggestion.go index 9c84ca442..94138893b 100644 --- a/shortcuts/calendar/calendar_suggestion.go +++ b/shortcuts/calendar/calendar_suggestion.go @@ -15,6 +15,8 @@ import ( larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/internal/errclass" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" ) @@ -70,11 +72,11 @@ func buildSuggestionRequest(runtime *common.RuntimeContext) (*SuggestionRequest, timeMin, err := common.ParseTime(startInput) if err != nil { - return nil, output.ErrValidation("invalid --start: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --start: %v", err).WithParam("--start") } minSec, err := strconv.ParseInt(timeMin, 10, 64) if err != nil { - return nil, output.ErrValidation("invalid start timestamp: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start timestamp: %v", err) } startTime := time.Unix(minSec, 0) @@ -87,12 +89,12 @@ func buildSuggestionRequest(runtime *common.RuntimeContext) (*SuggestionRequest, timeMax, err := common.ParseTime(endInput, "end") if err != nil { - return nil, output.ErrValidation("invalid --end: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --end: %v", err).WithParam("--end") } // Convert Unix timestamp string back to RFC3339 since the API requires RFC3339 maxSec, err := strconv.ParseInt(timeMax, 10, 64) if err != nil { - return nil, output.ErrValidation("invalid end timestamp: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end timestamp: %v", err) } req.SearchStartTime = startTime.Format(time.RFC3339) req.SearchEndTime = time.Unix(maxSec, 0).Format(time.RFC3339) @@ -157,23 +159,23 @@ func buildSuggestionRequest(runtime *common.RuntimeContext) (*SuggestionRequest, } parts := strings.Split(r, "~") if len(parts) != 2 { - return nil, output.ErrValidation("invalid --exclude format %q, expected 'start~end'", r) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --exclude format %q, expected 'start~end'", r).WithParam("--exclude") } startTsStr, err := common.ParseTime(parts[0]) if err != nil { - return nil, output.ErrValidation("invalid start time in --exclude: %q (%v)", parts[0], err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time in --exclude: %q (%v)", parts[0], err).WithParam("--exclude") } endTsStr, err := common.ParseTime(parts[1], "end") if err != nil { - return nil, output.ErrValidation("invalid end time in --exclude: %q (%v)", parts[1], err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time in --exclude: %q (%v)", parts[1], err).WithParam("--exclude") } startSec, err := strconv.ParseInt(startTsStr, 10, 64) if err != nil { - return nil, output.ErrValidation("invalid start timestamp in --exclude: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start timestamp in --exclude: %v", err).WithParam("--exclude") } endSec, err := strconv.ParseInt(endTsStr, 10, 64) if err != nil { - return nil, output.ErrValidation("invalid end timestamp in --exclude: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end timestamp in --exclude: %v", err).WithParam("--exclude") } excludedTimes = append(excludedTimes, &EventTime{ EventStartTime: time.Unix(startSec, 0).Format(time.RFC3339), @@ -219,13 +221,13 @@ var CalendarSuggestion = common.Shortcut{ } durationMinutes := runtime.Int(flagDurationMinutes) if durationMinutes != 0 && (durationMinutes < 1 || durationMinutes > 1440) { - return output.ErrValidation("--duration-minutes must be between 1 and 1440") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--duration-minutes must be between 1 and 1440").WithParam("--duration-minutes") } for _, flag := range []string{flagEventRrule, flagTimezone} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { - return output.ErrValidation(err.Error()) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err) } } } @@ -237,7 +239,7 @@ var CalendarSuggestion = common.Shortcut{ continue } if !strings.HasPrefix(id, "ou_") && !strings.HasPrefix(id, "oc_") { - return output.ErrValidation("invalid attendee id format %q: should start with 'ou_' or 'oc_'", id) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_' or 'oc_'", id) } } } @@ -245,14 +247,14 @@ var CalendarSuggestion = common.Shortcut{ startInput := runtime.Str(flagStart) if startInput != "" { if _, err := common.ParseTime(startInput); err != nil { - return output.ErrValidation("invalid start time: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start") } } endInput := runtime.Str(flagEnd) if endInput != "" { if _, err := common.ParseTime(endInput, "end"); err != nil { - return output.ErrValidation("invalid end time: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end") } } @@ -267,13 +269,13 @@ var CalendarSuggestion = common.Shortcut{ } parts := strings.Split(r, "~") if len(parts) != 2 { - return output.ErrValidation("invalid range format in --exclude: %q, expect start~end", r) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid range format in --exclude: %q, expect start~end", r).WithParam("--exclude") } if _, err := common.ParseTime(parts[0]); err != nil { - return output.ErrValidation("invalid start time in --exclude: %q (%v)", parts[0], err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time in --exclude: %q (%v)", parts[0], err).WithParam("--exclude") } if _, err := common.ParseTime(parts[1], "end"); err != nil { - return output.ErrValidation("invalid end time in --exclude: %q (%v)", parts[1], err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time in --exclude: %q (%v)", parts[1], err).WithParam("--exclude") } } } @@ -292,20 +294,23 @@ var CalendarSuggestion = common.Shortcut{ Body: req, }) if err != nil { - return err + if _, ok := errs.ProblemOf(err); ok { + return err + } + return errs.WrapInternal(err) } if apiResp.StatusCode < http.StatusOK || apiResp.StatusCode >= http.StatusMultipleChoices { - return output.ErrAPI(apiResp.StatusCode, "", string(apiResp.RawBody)) + return errs.NewAPIError(errs.SubtypeUnknown, "%s", string(apiResp.RawBody)).WithCode(apiResp.StatusCode) } var resp = &OpenAPIResponse[*SuggestionResponse]{} if err := json.Unmarshal(apiResp.RawBody, &resp); err != nil { - return output.ErrWithHint(output.ExitInternal, "validation", "unmarshal response fail", err.Error()) + return errs.NewInternalError(errs.SubtypeInvalidResponse, "unmarshal response fail").WithCause(err) } if resp.Code != 0 { - return output.ErrAPI(resp.Code, resp.Msg, resp.Data) + return errclass.BuildAPIError(map[string]any{"code": resp.Code, "msg": resp.Msg}, errclass.ClassifyContext{}) } data := resp.Data diff --git a/shortcuts/calendar/calendar_test.go b/shortcuts/calendar/calendar_test.go index 313788237..0d290cc03 100644 --- a/shortcuts/calendar/calendar_test.go +++ b/shortcuts/calendar/calendar_test.go @@ -12,6 +12,7 @@ import ( "sync" "testing" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/httpmock" @@ -1962,3 +1963,319 @@ func TestShortcuts_AllHaveScopes(t *testing.T) { } } } + +// --------------------------------------------------------------------------- +// Typed error shape tests (typed-errs migration pass 1) +// --------------------------------------------------------------------------- + +// Task 1: calendar_agenda.go +func TestAgenda_ParseTimeRange_InvalidStart_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarAgenda, []string{"+agenda", "--start", "not-a-time", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } + if ve.Param != "--start" { + t.Errorf("param=%q, want --start", ve.Param) + } +} + +// Task 2: calendar_create.go +func TestCreate_InvalidAttendeeID_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarCreate, []string{"+create", "--summary", "x", "--start", "2025-03-21T10:00:00+08:00", "--end", "2025-03-21T11:00:00+08:00", "--calendar-id", "cal_test123", "--attendee-ids", "bad_id", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } +} + +func TestCreate_NoEventID_TypedInternal(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{Method: "POST", URL: "/open-apis/calendar/v4/calendars/cal_test123/events", Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"event": map[string]interface{}{}}}}) + err := mountAndRun(t, CalendarCreate, []string{"+create", "--summary", "x", "--start", "2025-03-21T10:00:00+08:00", "--end", "2025-03-21T11:00:00+08:00", "--calendar-id", "cal_test123", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ie *errs.InternalError + if !errors.As(err, &ie) { + t.Fatalf("want *errs.InternalError, got %T", err) + } + if ie.Subtype != errs.SubtypeInvalidResponse { + t.Errorf("subtype=%q", ie.Subtype) + } +} + +// Task 3: calendar_freebusy.go +func TestFreebusy_InvalidStart_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarFreebusy, []string{"+freebusy", "--start", "not-a-time", "--user-id", "ou_someone", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } + if ve.Param != "--start" { + t.Errorf("param=%q, want --start", ve.Param) + } +} + +// Task 4: calendar_rsvp.go +func TestRsvp_EmptyEventID_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarRsvp, []string{"+rsvp", "--event-id", " ", "--rsvp-status", "accept", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } + if ve.Param != "--event-id" { + t.Errorf("param=%q, want --event-id", ve.Param) + } +} + +// Task 5: calendar_room_find.go +func TestRoomFind_MissingSlot_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarRoomFind, []string{"+room-find", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } + if ve.Param != "--slot" { + t.Errorf("param=%q, want --slot", ve.Param) + } +} + +func TestRoomFind_APICodeError_Typed(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{Method: "POST", URL: roomFindPath, Body: map[string]interface{}{"code": 99991, "msg": "boom"}}) + err := mountAndRun(t, CalendarRoomFind, []string{"+room-find", "--slot", "2025-03-21T10:00:00+08:00~2025-03-21T11:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ae *errs.APIError + if !errors.As(err, &ae) { + t.Fatalf("want *errs.APIError, got %T", err) + } + if ae.Subtype != errs.SubtypeUnknown { + t.Errorf("subtype=%q, want unknown", ae.Subtype) + } + if ae.Code != 99991 { + t.Errorf("code=%d, want 99991", ae.Code) + } + if output.ExitCodeOf(err) != output.ExitAPI { + t.Errorf("exit=%d want ExitAPI", output.ExitCodeOf(err)) + } +} + +func TestRoomFind_HTTPNon2xx_Typed(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{Method: "POST", URL: roomFindPath, Status: 500, Body: map[string]interface{}{"code": 500, "msg": "server error"}}) + err := mountAndRun(t, CalendarRoomFind, []string{"+room-find", "--slot", "2025-03-21T10:00:00+08:00~2025-03-21T11:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ae *errs.APIError + if !errors.As(err, &ae) { + t.Fatalf("want *errs.APIError, got %T", err) + } + if ae.Subtype != errs.SubtypeUnknown { + t.Errorf("subtype=%q, want unknown", ae.Subtype) + } + if ae.Code != 500 { + t.Errorf("code=%d, want 500", ae.Code) + } + if output.ExitCodeOf(err) != output.ExitAPI { + t.Errorf("exit=%d want ExitAPI", output.ExitCodeOf(err)) + } +} + +// Task 6: calendar_suggestion.go +func TestSuggestion_InvalidExclude_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarSuggestion, []string{"+suggestion", "--exclude", "not-a-range", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } + if ve.Param != "--exclude" { + t.Errorf("param=%q, want --exclude", ve.Param) + } +} + +func TestSuggestion_APICodeError_Typed(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{Method: "POST", URL: suggestionPath, Body: map[string]interface{}{"code": 99991, "msg": "boom"}}) + err := mountAndRun(t, CalendarSuggestion, []string{"+suggestion", "--start", "2025-03-21T10:00:00+08:00", "--end", "2025-03-21T11:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ae *errs.APIError + if !errors.As(err, &ae) { + t.Fatalf("want *errs.APIError, got %T", err) + } + if ae.Subtype != errs.SubtypeUnknown { + t.Errorf("subtype=%q, want unknown", ae.Subtype) + } + if ae.Code != 99991 { + t.Errorf("code=%d, want 99991", ae.Code) + } + if output.ExitCodeOf(err) != output.ExitAPI { + t.Errorf("exit=%d want ExitAPI", output.ExitCodeOf(err)) + } +} + +// Task 7: calendar_update.go +func TestUpdate_AttendeeConflict_Typed(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarUpdate, []string{"+update", "--event-id", "evt_1", "--add-attendee-ids", "ou_dup", "--remove-attendee-ids", "ou_dup", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q", ve.Subtype) + } + if ve.Param != "" { + t.Errorf("param=%q, want empty (cross-flag)", ve.Param) + } +} + +// The empty-event-id guard at executeCalendarUpdate is defensive: the Validate +// hook (validateCalendarUpdate) rejects an empty --event-id before Execute runs, +// so the :283 guard is unreachable through the normal CLI flow. Exercise it +// directly to pin the migrated typed shape (ValidationError / invalid_argument / +// --event-id). +func TestUpdate_EmptyEventID_Typed(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("calendar-id", "", "") + cmd.Flags().String("event-id", "", "") + runtime := common.TestNewRuntimeContextWithCtx(context.Background(), cmd, defaultConfig()) + err := executeCalendarUpdate(context.Background(), runtime) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q, want invalid_argument", ve.Subtype) + } + if ve.Param != "--event-id" { + t.Errorf("param=%q, want --event-id", ve.Param) + } +} + +// Round-1 completeness: FlagErrorf call sites migrated to typed errs. + +// calendar_create.go start/end validation block. +func TestCreate_MissingStart_TypedFlag(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + // --start is a Required flag; pass it empty to satisfy cobra's required-flag + // check and reach the in-builder empty-value guard. + err := mountAndRun(t, CalendarCreate, []string{"+create", "--summary", "x", "--calendar-id", "cal_test123", "--start", "", "--end", "2025-03-21T11:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q, want invalid_argument", ve.Subtype) + } + if ve.Param != "--start" { + t.Errorf("param=%q, want --start", ve.Param) + } +} + +// calendar_freebusy.go bot-identity guard. +func TestFreebusy_BotMissingUserID_TypedFlag(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarFreebusy, []string{"+freebusy", "--start", "2025-03-21T10:00:00+08:00", "--end", "2025-03-21T11:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q, want invalid_argument", ve.Subtype) + } + if ve.Param != "--user-id" { + t.Errorf("param=%q, want --user-id", ve.Param) + } +} + +// calendar_update.go buildCalendarUpdateEventData time-pairing guard. +func TestUpdate_StartWithoutEnd_TypedFlag(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarUpdate, []string{"+update", "--event-id", "evt_1", "--start", "2025-03-21T10:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("subtype=%q, want invalid_argument", ve.Subtype) + } +} + +// calendar_update.go invalid start-time guard carries the offending flag. +func TestUpdate_InvalidStartTime_TypedFlag(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, CalendarUpdate, []string{"+update", "--event-id", "evt_1", "--start", "not-a-time", "--end", "2025-03-21T11:00:00+08:00", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("want error") + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("want *errs.ValidationError, got %T", err) + } + if ve.Param != "--start" { + t.Errorf("param=%q, want --start", ve.Param) + } +} diff --git a/shortcuts/calendar/calendar_update.go b/shortcuts/calendar/calendar_update.go index f0b24415a..382db653f 100644 --- a/shortcuts/calendar/calendar_update.go +++ b/shortcuts/calendar/calendar_update.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/shortcuts/common" @@ -54,13 +55,13 @@ func validateCalendarUpdate(runtime *common.RuntimeContext) error { for _, flag := range []string{"event-id", "summary", "description", "rrule", "calendar-id", "start", "end", "add-attendee-ids", "remove-attendee-ids"} { if val := runtime.Str(flag); val != "" { if err := common.RejectDangerousChars("--"+flag, val); err != nil { - return output.ErrValidation(err.Error()) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err) } } } if strings.TrimSpace(runtime.Str("event-id")) == "" { - return common.FlagErrorf("specify --event-id") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --event-id").WithParam("--event-id") } if _, _, err := buildCalendarUpdateEventData(runtime); err != nil { return err @@ -69,7 +70,7 @@ func validateCalendarUpdate(runtime *common.RuntimeContext) error { return err } if !hasCalendarUpdateOperation(runtime) { - return common.FlagErrorf("nothing to update: specify at least one of --summary, --description, --start/--end, --rrule, --add-attendee-ids, or --remove-attendee-ids") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "nothing to update: specify at least one of --summary, --description, --start/--end, --rrule, --add-attendee-ids, or --remove-attendee-ids") } return nil } @@ -89,7 +90,7 @@ func validateCalendarUpdateAttendees(runtime *common.RuntimeContext) error { } for _, id := range addIDs { if _, ok := removeSet[id]; ok { - return output.ErrValidation("attendee id %q appears in both --add-attendee-ids and --remove-attendee-ids", id) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "attendee id %q appears in both --add-attendee-ids and --remove-attendee-ids", id) } } return nil @@ -124,27 +125,27 @@ func buildCalendarUpdateEventData(runtime *common.RuntimeContext) (map[string]in startChanged := runtime.Cmd.Flags().Changed("start") endChanged := runtime.Cmd.Flags().Changed("end") if startChanged != endChanged { - return nil, false, common.FlagErrorf("--start and --end must be specified together when updating event time") + return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "--start and --end must be specified together when updating event time") } if startChanged { startTs, err := common.ParseTime(runtime.Str("start")) if err != nil { - return nil, false, common.FlagErrorf("--start: %v", err) + return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "--start: %v", err).WithParam("--start") } endTs, err := common.ParseTime(runtime.Str("end"), "end") if err != nil { - return nil, false, common.FlagErrorf("--end: %v", err) + return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "--end: %v", err).WithParam("--end") } s, err := strconv.ParseInt(startTs, 10, 64) if err != nil { - return nil, false, common.FlagErrorf("invalid start time: %v", err) + return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid start time: %v", err).WithParam("--start") } e, err := strconv.ParseInt(endTs, 10, 64) if err != nil { - return nil, false, common.FlagErrorf("invalid end time: %v", err) + return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid end time: %v", err).WithParam("--end") } if e <= s { - return nil, false, common.FlagErrorf("end time must be after start time") + return nil, false, errs.NewValidationError(errs.SubtypeInvalidArgument, "end time must be after start time") } body["start_time"] = map[string]string{"timestamp": startTs} body["end_time"] = map[string]string{"timestamp": endTs} @@ -169,7 +170,7 @@ func parseCalendarAttendeeIDs(attendeesStr string) ([]string, error) { continue } if !strings.HasPrefix(id, "ou_") && !strings.HasPrefix(id, "oc_") && !strings.HasPrefix(id, "omm_") { - return nil, output.ErrValidation("invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id) } if _, ok := seen[id]; ok { continue @@ -195,7 +196,7 @@ func attendeeDeleteIDs(attendeesStr string) ([]map[string]string, error) { case strings.HasPrefix(id, "ou_"): deleteIDs = append(deleteIDs, map[string]string{"type": "user", "user_id": id}) default: - return nil, output.ErrValidation("invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id format %q: should start with 'ou_', 'oc_', or 'omm_'", id) } } return deleteIDs, nil @@ -280,7 +281,7 @@ func dryRunCalendarUpdate(runtime *common.RuntimeContext) *common.DryRunAPI { func executeCalendarUpdate(_ context.Context, runtime *common.RuntimeContext) error { calendarID, eventID := calendarUpdateIDs(runtime) if eventID == "" { - return output.ErrValidation("specify --event-id") + return errs.NewValidationError(errs.SubtypeInvalidArgument, "specify --event-id").WithParam("--event-id") } body, hasEventFields, err := buildCalendarUpdateEventData(runtime) @@ -291,6 +292,7 @@ func executeCalendarUpdate(_ context.Context, runtime *common.RuntimeContext) er completed := []string{} event := map[string]interface{}{} if hasEventFields { + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). data, err := runtime.CallAPI("PATCH", calendarUpdateEventPath(calendarID, eventID), map[string]interface{}{"user_id_type": "open_id"}, body) err = wrapPredefinedError(err) if err != nil { @@ -308,6 +310,7 @@ func executeCalendarUpdate(_ context.Context, runtime *common.RuntimeContext) er if err != nil { return err } + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). _, err = runtime.CallAPI("POST", calendarUpdateAttendeesPath(calendarID, eventID)+"/batch_delete", map[string]interface{}{"user_id_type": "open_id"}, map[string]interface{}{"delete_ids": deleteIDs, "need_notification": runtime.Bool("notify")}) @@ -323,8 +326,9 @@ func executeCalendarUpdate(_ context.Context, runtime *common.RuntimeContext) er if addStr := runtime.Str("add-attendee-ids"); strings.TrimSpace(addStr) != "" { attendees, err := parseAttendees(addStr, "") if err != nil { - return output.ErrValidation("invalid attendee id: %v", err) + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid attendee id: %v", err) } + // TODO: convert this API-error path to a typed errs.* error (kept on the legacy envelope for now). _, err = runtime.CallAPI("POST", calendarUpdateAttendeesPath(calendarID, eventID), map[string]interface{}{"user_id_type": "open_id"}, map[string]interface{}{"attendees": attendees, "need_notification": runtime.Bool("notify")})