From d553cbaa2de99d87c9d58eb3b2ded13bcb156377 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 08:18:28 +0000 Subject: [PATCH] fix(forge): correct generated schema aliases Co-Authored-By: Virgil --- cmd/forgegen/parser.go | 97 +++++++++++++++++++++++++++++++++---- cmd/forgegen/parser_test.go | 43 ++++++++++++++++ types/commit.go | 5 +- types/common.go | 6 +-- types/issue.go | 17 +++---- types/milestone.go | 2 +- types/misc.go | 3 +- types/notification.go | 4 +- types/pr.go | 11 ++--- types/quota.go | 14 ++---- types/review.go | 3 +- types/status.go | 4 +- types/user.go | 2 +- users_test.go | 2 +- 14 files changed, 160 insertions(+), 53 deletions(-) diff --git a/cmd/forgegen/parser.go b/cmd/forgegen/parser.go index 9a20fe2..a73a38f 100644 --- a/cmd/forgegen/parser.go +++ b/cmd/forgegen/parser.go @@ -39,6 +39,9 @@ type SpecInfo struct { // _ = SchemaDefinition{Type: "object"} type SchemaDefinition struct { Description string `json:"description"` + Format string `json:"format"` + Ref string `json:"$ref"` + Items *SchemaProperty `json:"items"` Type string `json:"type"` Properties map[string]SchemaProperty `json:"properties"` Required []string `json:"required"` @@ -140,9 +143,10 @@ func ExtractTypes(spec *Spec) map[string]*GoType { result[name] = gt continue } - if len(def.Properties) == 0 && def.AdditionalProperties != nil { + + if aliasType, ok := definitionAliasType(def, spec.Definitions); ok { gt.IsAlias = true - gt.AliasType = resolveMapType(*def.AdditionalProperties) + gt.AliasType = aliasType result[name] = gt continue } @@ -157,7 +161,7 @@ func ExtractTypes(spec *Spec) map[string]*GoType { } gf := GoField{ GoName: goName, - GoType: resolveGoType(prop), + GoType: resolveGoType(prop, spec.Definitions), JSONName: fieldName, Comment: prop.Description, Required: required[fieldName], @@ -172,6 +176,46 @@ func ExtractTypes(spec *Spec) map[string]*GoType { return result } +func definitionAliasType(def SchemaDefinition, defs map[string]SchemaDefinition) (string, bool) { + if def.Ref != "" { + return refName(def.Ref), true + } + + switch def.Type { + case "string": + return "string", true + case "integer": + switch def.Format { + case "int64": + return "int64", true + case "int32": + return "int32", true + default: + return "int", true + } + case "number": + switch def.Format { + case "float": + return "float32", true + default: + return "float64", true + } + case "boolean": + return "bool", true + case "array": + if def.Items != nil { + return "[]" + resolveGoType(*def.Items, defs), true + } + return "[]any", true + case "object": + if def.AdditionalProperties != nil { + return resolveMapType(*def.AdditionalProperties, defs), true + } + } + + return "", false +} + // DetectCRUDPairs finds Create*Option / Edit*Option pairs in the swagger definitions // and maps them back to the base type name. // @@ -201,10 +245,9 @@ func DetectCRUDPairs(spec *Spec) []CRUDPair { } // resolveGoType maps a swagger schema property to a Go type string. -func resolveGoType(prop SchemaProperty) string { +func resolveGoType(prop SchemaProperty, defs map[string]SchemaDefinition) string { if prop.Ref != "" { - parts := core.Split(prop.Ref, "/") - return "*" + parts[len(parts)-1] + return refGoType(prop.Ref, defs) } switch prop.Type { case "string": @@ -236,25 +279,59 @@ func resolveGoType(prop SchemaProperty) string { return "bool" case "array": if prop.Items != nil { - return "[]" + resolveGoType(*prop.Items) + return "[]" + resolveGoType(*prop.Items, defs) } return "[]any" case "object": - return resolveMapType(prop) + return resolveMapType(prop, defs) default: return "any" } } // resolveMapType maps a swagger object with additionalProperties to a Go map type. -func resolveMapType(prop SchemaProperty) string { +func resolveMapType(prop SchemaProperty, defs map[string]SchemaDefinition) string { valueType := "any" if prop.AdditionalProperties != nil { - valueType = resolveGoType(*prop.AdditionalProperties) + valueType = resolveGoType(*prop.AdditionalProperties, defs) } return "map[string]" + valueType } +func refName(ref string) string { + parts := core.Split(ref, "/") + return parts[len(parts)-1] +} + +func refGoType(ref string, defs map[string]SchemaDefinition) string { + name := refName(ref) + def, ok := defs[name] + if !ok { + return "*" + name + } + if definitionNeedsPointer(def) { + return "*" + name + } + return name +} + +func definitionNeedsPointer(def SchemaDefinition) bool { + if len(def.Enum) > 0 { + return false + } + if def.Ref != "" { + return false + } + switch def.Type { + case "string", "integer", "number", "boolean", "array": + return false + case "object": + return true + default: + return false + } +} + // pascalCase converts a snake_case or kebab-case string to PascalCase, // with common acronyms kept uppercase. func pascalCase(s string) string { diff --git a/cmd/forgegen/parser_test.go b/cmd/forgegen/parser_test.go index a18f009..e88da5f 100644 --- a/cmd/forgegen/parser_test.go +++ b/cmd/forgegen/parser_test.go @@ -124,3 +124,46 @@ func TestParser_AdditionalPropertiesAlias_Good(t *testing.T) { t.Fatalf("got alias type %q, want map[string]any", alias.AliasType) } } + +func TestParser_PrimitiveAndCollectionAliases_Good(t *testing.T) { + spec, err := LoadSpec("../../testdata/swagger.v1.json") + if err != nil { + t.Fatal(err) + } + + types := ExtractTypes(spec) + + cases := []struct { + name string + wantType string + }{ + {name: "CommitStatusState", wantType: "string"}, + {name: "IssueFormFieldType", wantType: "string"}, + {name: "IssueFormFieldVisible", wantType: "string"}, + {name: "NotifySubjectType", wantType: "string"}, + {name: "ReviewStateType", wantType: "string"}, + {name: "StateType", wantType: "string"}, + {name: "TimeStamp", wantType: "int64"}, + {name: "IssueTemplateLabels", wantType: "[]string"}, + {name: "QuotaGroupList", wantType: "[]*QuotaGroup"}, + {name: "QuotaUsedArtifactList", wantType: "[]*QuotaUsedArtifact"}, + {name: "QuotaUsedAttachmentList", wantType: "[]*QuotaUsedAttachment"}, + {name: "QuotaUsedPackageList", wantType: "[]*QuotaUsedPackage"}, + {name: "CreatePullReviewCommentOptions", wantType: "CreatePullReviewComment"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + gt, ok := types[tc.name] + if !ok { + t.Fatalf("type %q not found", tc.name) + } + if !gt.IsAlias { + t.Fatalf("type %q should be emitted as an alias", tc.name) + } + if gt.AliasType != tc.wantType { + t.Fatalf("type %q: got alias %q, want %q", tc.name, gt.AliasType, tc.wantType) + } + }) + } +} diff --git a/types/commit.go b/types/commit.go index c6ddbe0..e340f79 100644 --- a/types/commit.go +++ b/types/commit.go @@ -54,15 +54,14 @@ type CommitStatus struct { Creator *User `json:"creator,omitempty"` Description string `json:"description,omitempty"` ID int64 `json:"id,omitempty"` - Status *CommitStatusState `json:"status,omitempty"` + Status CommitStatusState `json:"status,omitempty"` TargetURL string `json:"target_url,omitempty"` URL string `json:"url,omitempty"` Updated time.Time `json:"updated_at,omitempty"` } // CommitStatusState — CommitStatusState holds the state of a CommitStatus It can be "pending", "success", "error" and "failure" -// CommitStatusState has no fields in the swagger spec. -type CommitStatusState struct{} +type CommitStatusState string type CommitUser struct { Date string `json:"date,omitempty"` diff --git a/types/common.go b/types/common.go index 5e0d84a..860e1e5 100644 --- a/types/common.go +++ b/types/common.go @@ -35,9 +35,7 @@ type Permission struct { } // StateType — StateType issue state type -// StateType has no fields in the swagger spec. -type StateType struct{} +type StateType string // TimeStamp — TimeStamp defines a timestamp -// TimeStamp has no fields in the swagger spec. -type TimeStamp struct{} +type TimeStamp int64 diff --git a/types/issue.go b/types/issue.go index 1249095..f5a9b25 100644 --- a/types/issue.go +++ b/types/issue.go @@ -91,7 +91,7 @@ type Issue struct { PullRequest *PullRequestMeta `json:"pull_request,omitempty"` Ref string `json:"ref,omitempty"` Repository *RepositoryMeta `json:"repository,omitempty"` - State *StateType `json:"state,omitempty"` + State StateType `json:"state,omitempty"` Title string `json:"title,omitempty"` URL string `json:"url,omitempty"` Updated time.Time `json:"updated_at,omitempty"` @@ -123,17 +123,15 @@ type IssueDeadline struct { type IssueFormField struct { Attributes map[string]any `json:"attributes,omitempty"` ID string `json:"id,omitempty"` - Type *IssueFormFieldType `json:"type,omitempty"` + Type IssueFormFieldType `json:"type,omitempty"` Validations map[string]any `json:"validations,omitempty"` - Visible []*IssueFormFieldVisible `json:"visible,omitempty"` + Visible []IssueFormFieldVisible `json:"visible,omitempty"` } -// IssueFormFieldType has no fields in the swagger spec. -type IssueFormFieldType struct{} +type IssueFormFieldType string // IssueFormFieldVisible — IssueFormFieldVisible defines issue form field visible -// IssueFormFieldVisible has no fields in the swagger spec. -type IssueFormFieldVisible struct{} +type IssueFormFieldVisible string // IssueLabelsOption — IssueLabelsOption a collection of labels // @@ -158,11 +156,10 @@ type IssueTemplate struct { Content string `json:"content,omitempty"` Fields []*IssueFormField `json:"body,omitempty"` FileName string `json:"file_name,omitempty"` - Labels *IssueTemplateLabels `json:"labels,omitempty"` + Labels IssueTemplateLabels `json:"labels,omitempty"` Name string `json:"name,omitempty"` Ref string `json:"ref,omitempty"` Title string `json:"title,omitempty"` } -// IssueTemplateLabels has no fields in the swagger spec. -type IssueTemplateLabels struct{} +type IssueTemplateLabels []string diff --git a/types/milestone.go b/types/milestone.go index e22aa1a..b91f75c 100644 --- a/types/milestone.go +++ b/types/milestone.go @@ -38,7 +38,7 @@ type Milestone struct { Description string `json:"description,omitempty"` ID int64 `json:"id,omitempty"` OpenIssues int64 `json:"open_issues,omitempty"` - State *StateType `json:"state,omitempty"` + State StateType `json:"state,omitempty"` Title string `json:"title,omitempty"` Updated time.Time `json:"updated_at,omitempty"` } diff --git a/types/misc.go b/types/misc.go index e7ab3d5..217d48e 100644 --- a/types/misc.go +++ b/types/misc.go @@ -213,8 +213,7 @@ type NewIssuePinsAllowed struct { } // NotifySubjectType — NotifySubjectType represent type of notification subject -// NotifySubjectType has no fields in the swagger spec. -type NotifySubjectType struct{} +type NotifySubjectType string // PRBranchInfo — PRBranchInfo information about a branch type PRBranchInfo struct { diff --git a/types/notification.go b/types/notification.go index 72e3381..9d188d1 100644 --- a/types/notification.go +++ b/types/notification.go @@ -15,9 +15,9 @@ type NotificationSubject struct { HTMLURL string `json:"html_url,omitempty"` LatestCommentHTMLURL string `json:"latest_comment_html_url,omitempty"` LatestCommentURL string `json:"latest_comment_url,omitempty"` - State *StateType `json:"state,omitempty"` + State StateType `json:"state,omitempty"` Title string `json:"title,omitempty"` - Type *NotifySubjectType `json:"type,omitempty"` + Type NotifySubjectType `json:"type,omitempty"` URL string `json:"url,omitempty"` } diff --git a/types/pr.go b/types/pr.go index 9077c3b..cac3d0e 100644 --- a/types/pr.go +++ b/types/pr.go @@ -39,8 +39,7 @@ type CreatePullReviewComment struct { // Usage: // // opts := CreatePullReviewCommentOptions{} -// CreatePullReviewCommentOptions has no fields in the swagger spec. -type CreatePullReviewCommentOptions struct{} +type CreatePullReviewCommentOptions CreatePullReviewComment // CreatePullReviewOptions — CreatePullReviewOptions are options to create a pull review // @@ -51,7 +50,7 @@ type CreatePullReviewOptions struct { Body string `json:"body,omitempty"` Comments []*CreatePullReviewComment `json:"comments,omitempty"` CommitID string `json:"commit_id,omitempty"` - Event *ReviewStateType `json:"event,omitempty"` + Event ReviewStateType `json:"event,omitempty"` } // EditPullRequestOption — EditPullRequestOption options when modify pull request @@ -107,7 +106,7 @@ type PullRequest struct { RequestedReviewers []*User `json:"requested_reviewers,omitempty"` RequestedReviewersTeams []*Team `json:"requested_reviewers_teams,omitempty"` ReviewComments int64 `json:"review_comments,omitempty"` // number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) - State *StateType `json:"state,omitempty"` + State StateType `json:"state,omitempty"` Title string `json:"title,omitempty"` URL string `json:"url,omitempty"` Updated time.Time `json:"updated_at,omitempty"` @@ -133,7 +132,7 @@ type PullReview struct { ID int64 `json:"id,omitempty"` Official bool `json:"official,omitempty"` Stale bool `json:"stale,omitempty"` - State *ReviewStateType `json:"state,omitempty"` + State ReviewStateType `json:"state,omitempty"` Submitted time.Time `json:"submitted_at,omitempty"` Team *Team `json:"team,omitempty"` Updated time.Time `json:"updated_at,omitempty"` @@ -176,5 +175,5 @@ type PullReviewRequestOptions struct { // opts := SubmitPullReviewOptions{Body: "example"} type SubmitPullReviewOptions struct { Body string `json:"body,omitempty"` - Event *ReviewStateType `json:"event,omitempty"` + Event ReviewStateType `json:"event,omitempty"` } diff --git a/types/quota.go b/types/quota.go index 592af82..2639a10 100644 --- a/types/quota.go +++ b/types/quota.go @@ -41,12 +41,11 @@ type QuotaGroup struct { } // QuotaGroupList — QuotaGroupList represents a list of quota groups -// QuotaGroupList has no fields in the swagger spec. -type QuotaGroupList struct{} +type QuotaGroupList []*QuotaGroup // QuotaInfo — QuotaInfo represents information about a user's quota type QuotaInfo struct { - Groups *QuotaGroupList `json:"groups,omitempty"` + Groups QuotaGroupList `json:"groups,omitempty"` Used *QuotaUsed `json:"used,omitempty"` } @@ -70,8 +69,7 @@ type QuotaUsedArtifact struct { } // QuotaUsedArtifactList — QuotaUsedArtifactList represents a list of artifacts counting towards a user's quota -// QuotaUsedArtifactList has no fields in the swagger spec. -type QuotaUsedArtifactList struct{} +type QuotaUsedArtifactList []*QuotaUsedArtifact // QuotaUsedAttachment — QuotaUsedAttachment represents an attachment counting towards a user's quota type QuotaUsedAttachment struct { @@ -82,8 +80,7 @@ type QuotaUsedAttachment struct { } // QuotaUsedAttachmentList — QuotaUsedAttachmentList represents a list of attachment counting towards a user's quota -// QuotaUsedAttachmentList has no fields in the swagger spec. -type QuotaUsedAttachmentList struct{} +type QuotaUsedAttachmentList []*QuotaUsedAttachment // QuotaUsedPackage — QuotaUsedPackage represents a package counting towards a user's quota type QuotaUsedPackage struct { @@ -95,8 +92,7 @@ type QuotaUsedPackage struct { } // QuotaUsedPackageList — QuotaUsedPackageList represents a list of packages counting towards a user's quota -// QuotaUsedPackageList has no fields in the swagger spec. -type QuotaUsedPackageList struct{} +type QuotaUsedPackageList []*QuotaUsedPackage // QuotaUsedSize — QuotaUsedSize represents the size-based quota usage of a user type QuotaUsedSize struct { diff --git a/types/review.go b/types/review.go index 3270e1b..e4ef6fa 100644 --- a/types/review.go +++ b/types/review.go @@ -4,5 +4,4 @@ package types // ReviewStateType — ReviewStateType review state type -// ReviewStateType has no fields in the swagger spec. -type ReviewStateType struct{} +type ReviewStateType string diff --git a/types/status.go b/types/status.go index 82cc42a..b1d432d 100644 --- a/types/status.go +++ b/types/status.go @@ -8,7 +8,7 @@ type CombinedStatus struct { CommitURL string `json:"commit_url,omitempty"` Repository *Repository `json:"repository,omitempty"` SHA string `json:"sha,omitempty"` - State *CommitStatusState `json:"state,omitempty"` + State CommitStatusState `json:"state,omitempty"` Statuses []*CommitStatus `json:"statuses,omitempty"` TotalCount int64 `json:"total_count,omitempty"` URL string `json:"url,omitempty"` @@ -22,6 +22,6 @@ type CombinedStatus struct { type CreateStatusOption struct { Context string `json:"context,omitempty"` Description string `json:"description,omitempty"` - State *CommitStatusState `json:"state,omitempty"` + State CommitStatusState `json:"state,omitempty"` TargetURL string `json:"target_url,omitempty"` } diff --git a/types/user.go b/types/user.go index c02c1aa..d157974 100644 --- a/types/user.go +++ b/types/user.go @@ -131,7 +131,7 @@ type User struct { // UserHeatmapData — UserHeatmapData represents the data needed to create a heatmap type UserHeatmapData struct { Contributions int64 `json:"contributions,omitempty"` - Timestamp *TimeStamp `json:"timestamp,omitempty"` + Timestamp TimeStamp `json:"timestamp,omitempty"` } // UserSettings — UserSettings represents user settings diff --git a/users_test.go b/users_test.go index 0488800..9fe78e2 100644 --- a/users_test.go +++ b/users_test.go @@ -138,7 +138,7 @@ func TestUserService_GetQuota_Good(t *testing.T) { t.Errorf("wrong path: %s", r.URL.Path) } json.NewEncoder(w).Encode(types.QuotaInfo{ - Groups: &types.QuotaGroupList{}, + Groups: types.QuotaGroupList{}, Used: &types.QuotaUsed{ Size: &types.QuotaUsedSize{ Repos: &types.QuotaUsedSizeRepos{