feat(forge): add safe stringers for option types
All checks were successful
Security Scan / security (push) Successful in 13s
Test / test (push) Successful in 1m49s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 08:07:18 +00:00
parent b8bc948fc0
commit bdf669db39
10 changed files with 348 additions and 0 deletions

View file

@ -35,6 +35,20 @@ type AdminActionsRunListOptions struct {
HeadSHA string 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 { func (o AdminActionsRunListOptions) queryParams() map[string]string {
query := make(map[string]string, 5) query := make(map[string]string, 5)
if o.Event != "" { if o.Event != "" {
@ -67,6 +81,14 @@ type AdminUnadoptedListOptions struct {
Pattern string 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 { func (o AdminUnadoptedListOptions) queryParams() map[string]string {
if o.Pattern == "" { if o.Pattern == "" {
return nil return nil

View file

@ -3,6 +3,7 @@ package forge
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
) )
func TestParams_String_Good(t *testing.T) { 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) 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)
}
})
}
}

View file

@ -1,7 +1,10 @@
package forge package forge
import ( import (
"fmt"
"strconv" "strconv"
"strings"
"time"
core "dappco.re/go/core" core "dappco.re/go/core"
) )
@ -25,6 +28,78 @@ func pathParams(values ...string) Params {
return 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 "<nil>"
}
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 { func lastIndexByte(s string, b byte) int {
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {
if s[i] == b { if s[i] == b {

View file

@ -31,6 +31,17 @@ type AttachmentUploadOptions struct {
UpdatedAt *time.Time 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. // RepoCommentListOptions controls filtering for repository-wide issue comment listings.
// //
// Usage: // Usage:
@ -41,6 +52,17 @@ type RepoCommentListOptions struct {
Before *time.Time 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 { func (o RepoCommentListOptions) queryParams() map[string]string {
query := make(map[string]string, 2) query := make(map[string]string, 2)
if o.Since != nil { if o.Since != nil {
@ -86,6 +108,30 @@ type SearchIssuesOptions struct {
Team string 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 { func (o SearchIssuesOptions) queryParams() map[string]string {
query := make(map[string]string, 12) query := make(map[string]string, 12)
if o.State != "" { if o.State != "" {

View file

@ -17,6 +17,14 @@ type MilestoneListOptions struct {
Name string 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 { func (o MilestoneListOptions) queryParams() map[string]string {
query := make(map[string]string, 2) query := make(map[string]string, 2)
if o.State != "" { if o.State != "" {

View file

@ -24,6 +24,20 @@ type NotificationListOptions struct {
Before *time.Time 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) { func (o NotificationListOptions) addQuery(values url.Values) {
if o.All { if o.All {
values.Set("all", "true") values.Set("all", "true")
@ -69,6 +83,19 @@ type NotificationRepoMarkOptions struct {
LastReadAt *time.Time 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. // NotificationMarkOptions controls how authenticated-user notifications are marked.
// //
// Usage: // Usage:
@ -81,6 +108,19 @@ type NotificationMarkOptions struct {
LastReadAt *time.Time 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 { func newNotificationService(c *Client) *NotificationService {
return &NotificationService{client: c} return &NotificationService{client: c}
} }

View file

@ -28,6 +28,14 @@ type OrgActivityFeedListOptions struct {
Date *time.Time 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 { func (o OrgActivityFeedListOptions) queryParams() map[string]string {
if o.Date == nil { if o.Date == nil {
return nil return nil

View file

@ -29,6 +29,17 @@ type ReleaseAttachmentUploadOptions struct {
ExternalURL string 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 { func releaseAttachmentUploadQuery(opts *ReleaseAttachmentUploadOptions) map[string]string {
if opts == nil || opts.Name == "" { if opts == nil || opts.Name == "" {
return nil return nil

View file

@ -31,6 +31,14 @@ type RepoKeyListOptions struct {
Fingerprint string 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 { func (o RepoKeyListOptions) queryParams() map[string]string {
query := make(map[string]string, 2) query := make(map[string]string, 2)
if o.KeyID != 0 { if o.KeyID != 0 {
@ -54,6 +62,14 @@ type ActivityFeedListOptions struct {
Date *time.Time 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 { func (o ActivityFeedListOptions) queryParams() map[string]string {
if o.Date == nil { if o.Date == nil {
return nil return nil
@ -74,6 +90,18 @@ type RepoTimeListOptions struct {
Before *time.Time 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 { func (o RepoTimeListOptions) queryParams() map[string]string {
query := make(map[string]string, 3) query := make(map[string]string, 3)
if o.User != "" { if o.User != "" {

View file

@ -29,6 +29,14 @@ type UserSearchOptions struct {
UID int64 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 { func (o UserSearchOptions) queryParams() map[string]string {
if o.UID == 0 { if o.UID == 0 {
return nil return nil
@ -47,6 +55,14 @@ type UserKeyListOptions struct {
Fingerprint string 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 { func (o UserKeyListOptions) queryParams() map[string]string {
if o.Fingerprint == "" { if o.Fingerprint == "" {
return nil return nil