Skip to content

Commit bdd632a

Browse files
committed
feat: 🎸 Separate models from interfaces
✅ Closes: #4
1 parent cbf53da commit bdd632a

File tree

9 files changed

+148
-16
lines changed

9 files changed

+148
-16
lines changed

‎cmd/sqlc-restruct/separate_interface.go‎

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,31 @@ var SeparateInterfaceCommand = &cli.Command{
1212
Flags: []cli.Flag{
1313
&cli.StringFlag{
1414
Name: "iface-pkg-name",
15-
Usage: "The package name where the separated models and Querier will be located.",
15+
Usage: "The package name where the separated Querier will be located.",
1616
Required: true,
1717
},
1818
&cli.StringFlag{
1919
Name: "iface-pkg-url",
20-
Usage: "The package URL where the separated models and Querier will be located (e.g. \"github.com/<user>/<repo>/path/to/pkg\").",
20+
Usage: "The package URL where the separated Querier will be located. (e.g. \"github.com/<user>/<repo>/path/to/pkg\")",
2121
Required: true,
2222
},
2323
&cli.StringFlag{
2424
Name: "iface-dir",
25-
Usage: "The directory path where the separated models and Querier will be located.",
25+
Usage: "The directory path where the separated Querier will be located.",
2626
Required: true,
2727
},
28+
&cli.StringFlag{
29+
Name: "models-pkg-name",
30+
Usage: "The package name where the separated models will be located. (default: --models-pkg-name value)",
31+
},
32+
&cli.StringFlag{
33+
Name: "models-pkg-url",
34+
Usage: "The package URL where the separated models will be located. (default: --models-pkg-url value)",
35+
},
36+
&cli.StringFlag{
37+
Name: "models-dir",
38+
Usage: "The directory path where the separated models will be located. (default: --iface-dir value)",
39+
},
2840
&cli.StringFlag{
2941
Name: "impl-dir",
3042
Usage: "The original directory where the sqlc-generated code is located.",
@@ -47,10 +59,30 @@ var SeparateInterfaceCommand = &cli.Command{
4759
},
4860
},
4961
Action: func(c *cli.Context) error {
62+
iPkgName := c.String("iface-pkg-name")
63+
iPkgURL := c.String("iface-pkg-url")
64+
iDir := c.String("iface-dir")
65+
66+
mPkgName := c.String("models-pkg-name")
67+
if mPkgName == "" {
68+
mPkgName = iPkgName
69+
}
70+
mPkgURL := c.String("models-pkg-url")
71+
if mPkgURL == "" {
72+
mPkgURL = iPkgURL
73+
}
74+
mDir := c.String("models-dir")
75+
if mDir == "" {
76+
mDir = iDir
77+
}
78+
5079
return separateinterface.Action(c.Context, separateinterface.ActionInput{
51-
IfacePkgName: c.String("iface-pkg-name"),
52-
IfacePkgURL: c.String("iface-pkg-url"),
53-
IfaceDir: c.String("iface-dir"),
80+
IfacePkgName: iPkgName,
81+
IfacePkgURL: iPkgURL,
82+
IfaceDir: iDir,
83+
ModelsPkgName: mPkgName,
84+
ModelsPkgURL: mPkgURL,
85+
ModelsDir: mDir,
5486
ImplDir: c.String("impl-dir"),
5587
ImplSQLSuffix: c.String("impl-sql-suffix"),
5688
ModelsFileName: c.String("models-file-name"),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!/.gitignore

‎example/infra/db/db.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package db
22

33
//go:generate docker compose run --rm -T sqlc generate
4-
//go:generate sqlc-restruct separate-interface --models-file-name=models.gen.go --querier-file-name=querier.gen.go --iface-dir=../../domain/repos --iface-pkg-name=repos --iface-pkg-url=github.com/mpyw/sqlc-restruct/example/domain/repos
4+
//go:generate sqlc-restruct separate-interface --models-file-name=models.gen.go --querier-file-name=querier.gen.go --iface-dir=../../domain/repos --iface-pkg-name=repos --iface-pkg-url=github.com/mpyw/sqlc-restruct/example/domain/repos --models-dir=../../domain/models --models-pkg-name=models --models-pkg-url=github.com/mpyw/sqlc-restruct/example/domain/models

‎example/migrations/20230101000000-create_users_table.sql‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
-- +migrate Up
2+
CREATE TYPE user_status AS ENUM ('active', 'inactive');
23
CREATE TABLE users(
34
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
45
email text NOT NULL UNIQUE,
56
name text NOT NULL,
7+
status user_status NOT NULL default 'active',
68
created_at timestamptz NOT NULL DEFAULT current_timestamp,
79
updated_at timestamptz NOT NULL DEFAULT current_timestamp
810
);
@@ -23,3 +25,4 @@ COMMENT ON COLUMN users.name IS 'Name';
2325

2426
-- +migrate Down
2527
DROP TABLE users;
28+
DROP TYPE user_statuses;

‎go.mod‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.20
55
require (
66
github.com/jackc/pgx/v5 v5.4.1
77
github.com/urfave/cli/v2 v2.25.7
8+
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
89
)
910

1011
require (

‎go.sum‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
2222
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
2323
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
2424
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
25+
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
26+
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
2527
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
2628
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
2729
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

‎pkg/actions/separate-interface/internal/astutil/astutil.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ func ExtractImportDecls(decls ...ast.Decl) []ast.Decl {
5151
}}
5252
}
5353

54+
func SymbolNameFromTypeOrValueDecls(decls ...ast.Decl) []string {
55+
var symbols []string
56+
for _, decl := range decls {
57+
switch decl := decl.(type) {
58+
case *ast.GenDecl:
59+
for _, spec := range decl.Specs {
60+
switch spec := spec.(type) {
61+
case *ast.ValueSpec:
62+
for _, name := range spec.Names {
63+
symbols = append(symbols, name.Name)
64+
}
65+
case *ast.TypeSpec:
66+
symbols = append(symbols, spec.Name.Name)
67+
}
68+
}
69+
}
70+
}
71+
return symbols
72+
}
73+
5474
func individualSpecs(exp bool, specs ...ast.Spec) []ast.Spec {
5575
var exported []ast.Spec
5676
for _, spec := range specs {
@@ -140,6 +160,19 @@ func (r *ExportedExprIdentUpdater) Visit(n ast.Node) ast.Visitor {
140160
n.Rhs[i] = rh
141161
}
142162
}
163+
case *ast.InterfaceType:
164+
ast.Inspect(n, func(n ast.Node) bool {
165+
switch n := n.(type) {
166+
case *ast.Field:
167+
if _, isInterfaceMethod := n.Type.(*ast.FuncType); !isInterfaceMethod {
168+
if expr := r.resolveExpr(n.Type); expr != nil {
169+
n.Type = expr
170+
}
171+
return false
172+
}
173+
}
174+
return true
175+
})
143176
}
144177
return r
145178
}

‎pkg/actions/separate-interface/runner.go‎

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,25 @@ import (
1313
"strings"
1414

1515
"github.com/mpyw/sqlc-restruct/pkg/actions/separate-interface/internal/astutil"
16+
"golang.org/x/exp/slices"
1617
)
1718

1819
type runner struct {
19-
input ActionInput
20-
fset *token.FileSet
20+
input ActionInput
21+
fset *token.FileSet
22+
exportedSymbolsInModels []string
2123
}
2224

2325
func (r *runner) Run() error {
2426
pkg, err := build.Import(".", r.input.ImplDir, build.IgnoreVendor)
2527
if err != nil {
2628
return fmt.Errorf("runner.Run() failed: %w", err)
2729
}
30+
f, err := parser.ParseFile(r.fset, path.Join(r.input.ImplDir, r.input.ModelsFileName), nil, parser.ParseComments)
31+
if err != nil {
32+
return fmt.Errorf("runner.Run() failed: %w", err)
33+
}
34+
r.exportedSymbolsInModels = astutil.SymbolNameFromTypeOrValueDecls(astutil.ExportedIndividualTypeOrValueDecls(f.Decls...)...)
2835

2936
var newModelsContent []byte
3037
var newQuerierContent []byte
@@ -58,8 +65,8 @@ func (r *runner) Run() error {
5865
}
5966

6067
if newModelsContent != nil {
61-
_ = os.Remove(path.Join(r.input.IfaceDir, r.input.ModelsFileName))
62-
if err := os.WriteFile(path.Join(r.input.IfaceDir, r.input.ModelsFileName), newModelsContent, 0644); err != nil {
68+
_ = os.Remove(path.Join(r.input.ModelsDir, r.input.ModelsFileName))
69+
if err := os.WriteFile(path.Join(r.input.ModelsDir, r.input.ModelsFileName), newModelsContent, 0644); err != nil {
6370
return fmt.Errorf("runner.Run() failed: %w", err)
6471
}
6572
_ = os.Remove(path.Join(r.input.ImplDir, r.input.ModelsFileName))
@@ -96,7 +103,7 @@ func (r *runner) newModelsContent() ([]byte, error) {
96103
}
97104

98105
// Change package name of "models" file
99-
f.Name = ast.NewIdent(r.input.IfacePkgName)
106+
f.Name = ast.NewIdent(r.input.ModelsPkgName)
100107

101108
byt, err := r.intoBytes(f)
102109
if err != nil {
@@ -114,6 +121,19 @@ func (r *runner) newQuerierContent() ([]byte, error) {
114121
// Change package name of "querier" file
115122
f.Name = ast.NewIdent(r.input.IfacePkgName)
116123

124+
// Prepend import statement of ModelsPkgURL
125+
if r.input.ModelsPkgURL != r.input.IfacePkgURL {
126+
f.Decls = append(append(([]ast.Decl)(nil), &ast.GenDecl{
127+
Tok: token.IMPORT,
128+
Specs: []ast.Spec{&ast.ImportSpec{
129+
Path: &ast.BasicLit{
130+
Kind: token.STRING,
131+
Value: fmt.Sprintf("%#v", r.input.ModelsPkgURL),
132+
},
133+
}},
134+
}), f.Decls...)
135+
}
136+
117137
// Remove top level constraint: var _ Querier = (*Querier)(nil)
118138
for i, decl := range f.Decls {
119139
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.VAR {
@@ -126,6 +146,22 @@ func (r *runner) newQuerierContent() ([]byte, error) {
126146
}
127147
}
128148

149+
// Qualify exported references
150+
if r.input.ModelsPkgURL != r.input.IfacePkgURL {
151+
ast.Walk(
152+
astutil.NewExportedExprIdentUpdater(func(ident *ast.Ident) ast.Expr {
153+
if slices.Contains(r.exportedSymbolsInModels, ident.Name) {
154+
return &ast.SelectorExpr{
155+
X: ast.NewIdent(r.input.ModelsPkgName),
156+
Sel: ident,
157+
}
158+
}
159+
return nil
160+
}),
161+
f,
162+
)
163+
}
164+
129165
dirEntries, err := os.ReadDir(r.input.ImplDir)
130166
if err != nil {
131167
return nil, fmt.Errorf("runner.newQuerierContent() failed: %w", err)
@@ -172,11 +208,28 @@ func (r *runner) newQueriesContent(filename string) ([]byte, error) {
172208
}},
173209
}), f.Decls...)
174210

211+
// Prepend import statement of ModelsPkgURL
212+
if r.input.ModelsPkgURL != r.input.IfacePkgURL {
213+
f.Decls = append(append(([]ast.Decl)(nil), &ast.GenDecl{
214+
Tok: token.IMPORT,
215+
Specs: []ast.Spec{&ast.ImportSpec{
216+
Path: &ast.BasicLit{
217+
Kind: token.STRING,
218+
Value: fmt.Sprintf("%#v", r.input.ModelsPkgURL),
219+
},
220+
}},
221+
}), f.Decls...)
222+
}
223+
175224
// Qualify exported references
176225
ast.Walk(
177226
astutil.NewExportedExprIdentUpdater(func(ident *ast.Ident) ast.Expr {
227+
pkgName := r.input.IfacePkgName
228+
if slices.Contains(r.exportedSymbolsInModels, ident.Name) {
229+
pkgName = r.input.ModelsPkgName
230+
}
178231
return &ast.SelectorExpr{
179-
X: ast.NewIdent(r.input.IfacePkgName),
232+
X: ast.NewIdent(pkgName),
180233
Sel: ident,
181234
}
182235
}),

‎pkg/actions/separate-interface/separate_interface.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ import (
88
)
99

1010
type ActionInput struct {
11-
// IfacePkgName The package name where the separated models and `Querier` will be located.
11+
// IfacePkgName The package name where the separated Querier will be located.
1212
IfacePkgName string
13-
// IfacePkgURL The package URL where the separated models and `Querier` will be located (e.g. "github.com/<user>/<repo>/path/to/pkg").
13+
// IfacePkgURL The package URL where the separated Querier will be located.
1414
IfacePkgURL string
15-
// IfaceDir The directory path where the separated models and `Querier` will be located.
15+
// IfaceDir The directory path where the separated Querier will be located.
1616
IfaceDir string
17+
// ModelsPkgName The package name where the separated models will be located.
18+
ModelsPkgName string
19+
// ModelsPkgURL The package URL where the separated models will be located.
20+
ModelsPkgURL string
21+
// ModelsDir The directory path where the separated models will be located.
22+
ModelsDir string
1723
// ImplDir The original directory where the sqlc-generated code is located.
1824
ImplDir string
1925
// ImplSQLSuffix The suffix for sqlc-generated files from SQL files.

0 commit comments

Comments
 (0)