From bdf669db39db0513af76e625139e93f7fcd8e56a Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 08:07:18 +0000 Subject: [PATCH] feat(forge): add safe stringers for option types Co-Authored-By: Virgil --- admin.go | 22 +++++++++++ ax_stringer_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 75 ++++++++++++++++++++++++++++++++++++ issues.go | 46 ++++++++++++++++++++++ milestones.go | 8 ++++ notifications.go | 40 +++++++++++++++++++ orgs.go | 8 ++++ releases.go | 11 ++++++ repos.go | 28 ++++++++++++++ users.go | 16 ++++++++ 10 files changed, 348 insertions(+) diff --git a/admin.go b/admin.go index 779f73e..c7f7f19 100644 --- a/admin.go +++ b/admin.go @@ -35,6 +35,20 @@ type AdminActionsRunListOptions struct { HeadSHA string } +// String returns a safe summary of the admin Actions run filters. +func (o AdminActionsRunListOptions) String() string { + return optionString("forge.AdminActionsRunListOptions", + "event", o.Event, + "branch", o.Branch, + "status", o.Status, + "actor", o.Actor, + "head_sha", o.HeadSHA, + ) +} + +// GoString returns a safe Go-syntax summary of the admin Actions run filters. +func (o AdminActionsRunListOptions) GoString() string { return o.String() } + func (o AdminActionsRunListOptions) queryParams() map[string]string { query := make(map[string]string, 5) if o.Event != "" { @@ -67,6 +81,14 @@ type AdminUnadoptedListOptions struct { Pattern string } +// String returns a safe summary of the unadopted repository filters. +func (o AdminUnadoptedListOptions) String() string { + return optionString("forge.AdminUnadoptedListOptions", "pattern", o.Pattern) +} + +// GoString returns a safe Go-syntax summary of the unadopted repository filters. +func (o AdminUnadoptedListOptions) GoString() string { return o.String() } + func (o AdminUnadoptedListOptions) queryParams() map[string]string { if o.Pattern == "" { return nil diff --git a/ax_stringer_test.go b/ax_stringer_test.go index 167bc02..443d791 100644 --- a/ax_stringer_test.go +++ b/ax_stringer_test.go @@ -3,6 +3,7 @@ package forge import ( "fmt" "testing" + "time" ) func TestParams_String_Good(t *testing.T) { @@ -79,3 +80,96 @@ func TestPagedResult_String_Good(t *testing.T) { t.Fatalf("got GoString=%q, want %q", got, want) } } + +func TestOption_Stringers_Good(t *testing.T) { + when := time.Date(2026, time.April, 2, 8, 3, 4, 0, time.UTC) + + cases := []struct { + name string + got fmt.Stringer + want string + }{ + { + name: "AdminActionsRunListOptions", + got: AdminActionsRunListOptions{Event: "push", Status: "success"}, + want: `forge.AdminActionsRunListOptions{event="push", status="success"}`, + }, + { + name: "AttachmentUploadOptions", + got: AttachmentUploadOptions{Name: "screenshot.png", UpdatedAt: &when}, + want: `forge.AttachmentUploadOptions{name="screenshot.png", updated_at="2026-04-02T08:03:04Z"}`, + }, + { + name: "NotificationListOptions", + got: NotificationListOptions{All: true, StatusTypes: []string{"unread"}, SubjectTypes: []string{"issue"}}, + want: `forge.NotificationListOptions{all=true, status_types=[]string{"unread"}, subject_types=[]string{"issue"}}`, + }, + { + name: "SearchIssuesOptions", + got: SearchIssuesOptions{State: "open", PriorityRepoID: 99, Assigned: true, Query: "build"}, + want: `forge.SearchIssuesOptions{state="open", q="build", priority_repo_id=99, assigned=true}`, + }, + { + name: "ReleaseAttachmentUploadOptions", + got: ReleaseAttachmentUploadOptions{Name: "release.zip"}, + want: `forge.ReleaseAttachmentUploadOptions{name="release.zip"}`, + }, + { + name: "UserSearchOptions", + got: UserSearchOptions{UID: 1001}, + want: `forge.UserSearchOptions{uid=1001}`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.got.String(); got != tc.want { + t.Fatalf("got String()=%q, want %q", got, tc.want) + } + if got := fmt.Sprint(tc.got); got != tc.want { + t.Fatalf("got fmt.Sprint=%q, want %q", got, tc.want) + } + if got := fmt.Sprintf("%#v", tc.got); got != tc.want { + t.Fatalf("got GoString=%q, want %q", got, tc.want) + } + }) + } +} + +func TestOption_Stringers_Empty(t *testing.T) { + cases := []struct { + name string + got fmt.Stringer + want string + }{ + { + name: "AdminUnadoptedListOptions", + got: AdminUnadoptedListOptions{}, + want: `forge.AdminUnadoptedListOptions{}`, + }, + { + name: "MilestoneListOptions", + got: MilestoneListOptions{}, + want: `forge.MilestoneListOptions{}`, + }, + { + name: "UserKeyListOptions", + got: UserKeyListOptions{}, + want: `forge.UserKeyListOptions{}`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.got.String(); got != tc.want { + t.Fatalf("got String()=%q, want %q", got, tc.want) + } + if got := fmt.Sprint(tc.got); got != tc.want { + t.Fatalf("got fmt.Sprint=%q, want %q", got, tc.want) + } + if got := fmt.Sprintf("%#v", tc.got); got != tc.want { + t.Fatalf("got GoString=%q, want %q", got, tc.want) + } + }) + } +} diff --git a/helpers.go b/helpers.go index 936af77..7f08cd1 100644 --- a/helpers.go +++ b/helpers.go @@ -1,7 +1,10 @@ package forge import ( + "fmt" "strconv" + "strings" + "time" core "dappco.re/go/core" ) @@ -25,6 +28,78 @@ func pathParams(values ...string) Params { return params } +func optionString(typeName string, fields ...any) string { + var b strings.Builder + b.WriteString(typeName) + b.WriteString("{") + + wroteField := false + for i := 0; i+1 < len(fields); i += 2 { + name, _ := fields[i].(string) + value := fields[i+1] + if isZeroOptionValue(value) { + continue + } + if wroteField { + b.WriteString(", ") + } + wroteField = true + b.WriteString(name) + b.WriteString("=") + b.WriteString(formatOptionValue(value)) + } + + b.WriteString("}") + return b.String() +} + +func isZeroOptionValue(v any) bool { + switch x := v.(type) { + case nil: + return true + case string: + return x == "" + case bool: + return !x + case int: + return x == 0 + case int64: + return x == 0 + case []string: + return len(x) == 0 + case *time.Time: + return x == nil + case time.Time: + return x.IsZero() + default: + return false + } +} + +func formatOptionValue(v any) string { + switch x := v.(type) { + case string: + return strconv.Quote(x) + case bool: + return strconv.FormatBool(x) + case int: + return strconv.Itoa(x) + case int64: + return strconv.FormatInt(x, 10) + case []string: + return fmt.Sprintf("%#v", x) + case *time.Time: + if x == nil { + return "" + } + return strconv.Quote(x.Format(time.RFC3339)) + case time.Time: + return strconv.Quote(x.Format(time.RFC3339)) + default: + return fmt.Sprintf("%#v", v) + } +} + func lastIndexByte(s string, b byte) int { for i := len(s) - 1; i >= 0; i-- { if s[i] == b { diff --git a/issues.go b/issues.go index 3d56461..177866a 100644 --- a/issues.go +++ b/issues.go @@ -31,6 +31,17 @@ type AttachmentUploadOptions struct { UpdatedAt *time.Time } +// String returns a safe summary of the attachment upload metadata. +func (o AttachmentUploadOptions) String() string { + return optionString("forge.AttachmentUploadOptions", + "name", o.Name, + "updated_at", o.UpdatedAt, + ) +} + +// GoString returns a safe Go-syntax summary of the attachment upload metadata. +func (o AttachmentUploadOptions) GoString() string { return o.String() } + // RepoCommentListOptions controls filtering for repository-wide issue comment listings. // // Usage: @@ -41,6 +52,17 @@ type RepoCommentListOptions struct { Before *time.Time } +// String returns a safe summary of the repository comment filters. +func (o RepoCommentListOptions) String() string { + return optionString("forge.RepoCommentListOptions", + "since", o.Since, + "before", o.Before, + ) +} + +// GoString returns a safe Go-syntax summary of the repository comment filters. +func (o RepoCommentListOptions) GoString() string { return o.String() } + func (o RepoCommentListOptions) queryParams() map[string]string { query := make(map[string]string, 2) if o.Since != nil { @@ -86,6 +108,30 @@ type SearchIssuesOptions struct { Team string } +// String returns a safe summary of the issue search filters. +func (o SearchIssuesOptions) String() string { + return optionString("forge.SearchIssuesOptions", + "state", o.State, + "labels", o.Labels, + "milestones", o.Milestones, + "q", o.Query, + "priority_repo_id", o.PriorityRepoID, + "type", o.Type, + "since", o.Since, + "before", o.Before, + "assigned", o.Assigned, + "created", o.Created, + "mentioned", o.Mentioned, + "review_requested", o.ReviewRequested, + "reviewed", o.Reviewed, + "owner", o.Owner, + "team", o.Team, + ) +} + +// GoString returns a safe Go-syntax summary of the issue search filters. +func (o SearchIssuesOptions) GoString() string { return o.String() } + func (o SearchIssuesOptions) queryParams() map[string]string { query := make(map[string]string, 12) if o.State != "" { diff --git a/milestones.go b/milestones.go index e560f6a..bdf1050 100644 --- a/milestones.go +++ b/milestones.go @@ -17,6 +17,14 @@ type MilestoneListOptions struct { Name string } +// String returns a safe summary of the milestone filters. +func (o MilestoneListOptions) String() string { + return optionString("forge.MilestoneListOptions", "state", o.State, "name", o.Name) +} + +// GoString returns a safe Go-syntax summary of the milestone filters. +func (o MilestoneListOptions) GoString() string { return o.String() } + func (o MilestoneListOptions) queryParams() map[string]string { query := make(map[string]string, 2) if o.State != "" { diff --git a/notifications.go b/notifications.go index 5e84dc7..661de6d 100644 --- a/notifications.go +++ b/notifications.go @@ -24,6 +24,20 @@ type NotificationListOptions struct { Before *time.Time } +// String returns a safe summary of the notification filters. +func (o NotificationListOptions) String() string { + return optionString("forge.NotificationListOptions", + "all", o.All, + "status_types", o.StatusTypes, + "subject_types", o.SubjectTypes, + "since", o.Since, + "before", o.Before, + ) +} + +// GoString returns a safe Go-syntax summary of the notification filters. +func (o NotificationListOptions) GoString() string { return o.String() } + func (o NotificationListOptions) addQuery(values url.Values) { if o.All { values.Set("all", "true") @@ -69,6 +83,19 @@ type NotificationRepoMarkOptions struct { LastReadAt *time.Time } +// String returns a safe summary of the repository notification mark options. +func (o NotificationRepoMarkOptions) String() string { + return optionString("forge.NotificationRepoMarkOptions", + "all", o.All, + "status_types", o.StatusTypes, + "to_status", o.ToStatus, + "last_read_at", o.LastReadAt, + ) +} + +// GoString returns a safe Go-syntax summary of the repository notification mark options. +func (o NotificationRepoMarkOptions) GoString() string { return o.String() } + // NotificationMarkOptions controls how authenticated-user notifications are marked. // // Usage: @@ -81,6 +108,19 @@ type NotificationMarkOptions struct { LastReadAt *time.Time } +// String returns a safe summary of the authenticated-user notification mark options. +func (o NotificationMarkOptions) String() string { + return optionString("forge.NotificationMarkOptions", + "all", o.All, + "status_types", o.StatusTypes, + "to_status", o.ToStatus, + "last_read_at", o.LastReadAt, + ) +} + +// GoString returns a safe Go-syntax summary of the authenticated-user notification mark options. +func (o NotificationMarkOptions) GoString() string { return o.String() } + func newNotificationService(c *Client) *NotificationService { return &NotificationService{client: c} } diff --git a/orgs.go b/orgs.go index 0b655b1..c76540f 100644 --- a/orgs.go +++ b/orgs.go @@ -28,6 +28,14 @@ type OrgActivityFeedListOptions struct { Date *time.Time } +// String returns a safe summary of the organisation activity feed filters. +func (o OrgActivityFeedListOptions) String() string { + return optionString("forge.OrgActivityFeedListOptions", "date", o.Date) +} + +// GoString returns a safe Go-syntax summary of the organisation activity feed filters. +func (o OrgActivityFeedListOptions) GoString() string { return o.String() } + func (o OrgActivityFeedListOptions) queryParams() map[string]string { if o.Date == nil { return nil diff --git a/releases.go b/releases.go index d530305..5e58496 100644 --- a/releases.go +++ b/releases.go @@ -29,6 +29,17 @@ type ReleaseAttachmentUploadOptions struct { ExternalURL string } +// String returns a safe summary of the release attachment upload metadata. +func (o ReleaseAttachmentUploadOptions) String() string { + return optionString("forge.ReleaseAttachmentUploadOptions", + "name", o.Name, + "external_url", o.ExternalURL, + ) +} + +// GoString returns a safe Go-syntax summary of the release attachment upload metadata. +func (o ReleaseAttachmentUploadOptions) GoString() string { return o.String() } + func releaseAttachmentUploadQuery(opts *ReleaseAttachmentUploadOptions) map[string]string { if opts == nil || opts.Name == "" { return nil diff --git a/repos.go b/repos.go index a266285..08f9fee 100644 --- a/repos.go +++ b/repos.go @@ -31,6 +31,14 @@ type RepoKeyListOptions struct { Fingerprint string } +// String returns a safe summary of the repository key filters. +func (o RepoKeyListOptions) String() string { + return optionString("forge.RepoKeyListOptions", "key_id", o.KeyID, "fingerprint", o.Fingerprint) +} + +// GoString returns a safe Go-syntax summary of the repository key filters. +func (o RepoKeyListOptions) GoString() string { return o.String() } + func (o RepoKeyListOptions) queryParams() map[string]string { query := make(map[string]string, 2) if o.KeyID != 0 { @@ -54,6 +62,14 @@ type ActivityFeedListOptions struct { Date *time.Time } +// String returns a safe summary of the activity feed filters. +func (o ActivityFeedListOptions) String() string { + return optionString("forge.ActivityFeedListOptions", "date", o.Date) +} + +// GoString returns a safe Go-syntax summary of the activity feed filters. +func (o ActivityFeedListOptions) GoString() string { return o.String() } + func (o ActivityFeedListOptions) queryParams() map[string]string { if o.Date == nil { return nil @@ -74,6 +90,18 @@ type RepoTimeListOptions struct { Before *time.Time } +// String returns a safe summary of the tracked time filters. +func (o RepoTimeListOptions) String() string { + return optionString("forge.RepoTimeListOptions", + "user", o.User, + "since", o.Since, + "before", o.Before, + ) +} + +// GoString returns a safe Go-syntax summary of the tracked time filters. +func (o RepoTimeListOptions) GoString() string { return o.String() } + func (o RepoTimeListOptions) queryParams() map[string]string { query := make(map[string]string, 3) if o.User != "" { diff --git a/users.go b/users.go index 3f90f12..86d9125 100644 --- a/users.go +++ b/users.go @@ -29,6 +29,14 @@ type UserSearchOptions struct { UID int64 } +// String returns a safe summary of the user search filters. +func (o UserSearchOptions) String() string { + return optionString("forge.UserSearchOptions", "uid", o.UID) +} + +// GoString returns a safe Go-syntax summary of the user search filters. +func (o UserSearchOptions) GoString() string { return o.String() } + func (o UserSearchOptions) queryParams() map[string]string { if o.UID == 0 { return nil @@ -47,6 +55,14 @@ type UserKeyListOptions struct { Fingerprint string } +// String returns a safe summary of the user key filters. +func (o UserKeyListOptions) String() string { + return optionString("forge.UserKeyListOptions", "fingerprint", o.Fingerprint) +} + +// GoString returns a safe Go-syntax summary of the user key filters. +func (o UserKeyListOptions) GoString() string { return o.String() } + func (o UserKeyListOptions) queryParams() map[string]string { if o.Fingerprint == "" { return nil