fix(forge): correct generated schema aliases
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Successful in 2m6s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 08:18:28 +00:00
parent dba9852567
commit d553cbaa2d
14 changed files with 160 additions and 53 deletions

View file

@ -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 {

View file

@ -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)
}
})
}
}

View file

@ -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"`

View file

@ -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

View file

@ -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

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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"`
}

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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

View file

@ -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"`
}

View file

@ -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

View file

@ -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{