Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions structs/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Field struct {
type templateData struct {
HeaderComment string
PackageName string
Imports []string
SourceName string
OutputName string
Fields []Field
Expand Down Expand Up @@ -100,6 +101,7 @@ func flattenOne(f *ast.File, cfg StructConfig, typeKinds map[string]string, scal
data := templateData{
HeaderComment: o.headerComment,
PackageName: o.packageName,
Imports: collectImports(fields),
SourceName: cfg.SourceName,
OutputName: cfg.OutputName,
Fields: fields,
Expand Down Expand Up @@ -128,6 +130,38 @@ func flattenOne(f *ast.File, cfg StructConfig, typeKinds map[string]string, scal
return nil
}

// collectImports extracts unique package import paths from field types that
// contain a dot (e.g. "time.Time" -> "time"). Returns a sorted, deduplicated list.
func collectImports(fields []Field) []string {
// Map well-known qualified type prefixes to their import paths.
pkgToImport := map[string]string{
"time": "time",
"net": "net",
"url": "net/url",
"json": "encoding/json",
"uuid": "github.com/google/uuid",
}

seen := map[string]bool{}
for _, f := range fields {
typ := strings.TrimPrefix(f.Type, "*")
typ = strings.TrimPrefix(typ, "[]")
if dot := strings.IndexByte(typ, '.'); dot > 0 {
pkg := typ[:dot]
if imp, ok := pkgToImport[pkg]; ok && !seen[imp] {
seen[imp] = true
}
}
}

imports := make([]string, 0, len(seen))
for imp := range seen {
imports = append(imports, imp)
}
slices.Sort(imports)
return imports
}

// sortFields sorts fields with "ID" first, then alphabetically by name (case-insensitive).
func sortFields(fields []Field) {
slices.SortStableFunc(fields, func(a, b Field) int {
Expand Down
7 changes: 6 additions & 1 deletion structs/flatten_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,13 @@ func TestResolveType(t *testing.T) {
want: "[]map[string]any",
},
{
name: "cross-package type",
name: "cross-package scalar (time.Time)",
expr: &ast.SelectorExpr{X: &ast.Ident{Name: "time"}, Sel: &ast.Ident{Name: "Time"}},
want: "time.Time",
},
{
name: "cross-package struct",
expr: &ast.SelectorExpr{X: &ast.Ident{Name: "api"}, Sel: &ast.Ident{Name: "Finding"}},
want: "map[string]any",
},
{
Expand Down
25 changes: 24 additions & 1 deletion structs/options.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
package structs

// defaultScalarKinds are Go types that are inexpensive to decode and should stay typed.
// Includes both unqualified names (for types in the same package) and
// fully qualified names like "time.Time" (for cross-package types).
var defaultScalarKinds = map[string]bool{
"string": true,
"bool": true,
"float32": true,
"float64": true,
"int64": true,
"int": true,
"int8": true,
"int16": true,
"int32": true,
"int64": true,
"uint": true,
"uint8": true,
"uint16": true,
"uint32": true,
"uint64": true,
"byte": true,
"rune": true,

"time.Time": true,
"time.Duration": true,
"net.IP": true,
"net.HardwareAddr": true,
"url.URL": true,
"json.RawMessage": true,
"json.Number": true,

"uuid.UUID": true,
}

type options struct {
Expand Down
12 changes: 11 additions & 1 deletion structs/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,18 @@ func resolveType(expr ast.Expr, typeKinds map[string]string, scalars map[string]
}
return "[]" + inner

case *ast.SelectorExpr:
// Cross-package types like time.Time.
if pkg, ok := t.X.(*ast.Ident); ok {
qualified := pkg.Name + "." + t.Sel.Name
if scalars[qualified] {
return qualified
}
}
return "map[string]any"

default:
// SelectorExpr (cross-package types), MapType, InterfaceType, etc.
// MapType, InterfaceType, etc.
return "map[string]any"
}
}
Expand Down
8 changes: 7 additions & 1 deletion structs/templates/struct.go.tpl
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// {{ .HeaderComment }}

package {{ .PackageName }}

{{ if .Imports }}
import (
{{- range .Imports }}
"{{ . }}"
{{- end }}
)
{{ end }}
// {{ .OutputName }} is an optimized version of {{ .SourceName }}
// where deeply nested struct fields are replaced with map[string]any to avoid
// the decode -> allocate -> re-encode cycle.
Expand Down
Loading