From 737c1a4044efe1c0c98573072a6dbc6387c2437a Mon Sep 17 00:00:00 2001 From: sunboyy Date: Mon, 17 Oct 2022 17:57:49 +0700 Subject: [PATCH] Scan directory to find source code in multiple files in the same package (#30) --- .github/workflows/build.yml | 16 +-- .github/workflows/lint.yml | 10 +- go.mod | 21 ++- go.sum | 61 +++----- internal/code/errors.go | 28 ++++ internal/code/errors_test.go | 36 +++++ internal/code/extractor_test.go | 16 +-- internal/code/models.go | 32 +---- internal/code/models_test.go | 54 -------- internal/code/package.go | 66 +++++++++ internal/code/package_test.go | 238 ++++++++++++++++++++++++++++++++ internal/spec/field.go | 4 +- internal/spec/parser.go | 2 +- internal/spec/parser_test.go | 6 +- main.go | 31 +++-- 15 files changed, 444 insertions(+), 177 deletions(-) create mode 100644 internal/code/errors.go create mode 100644 internal/code/errors_test.go create mode 100644 internal/code/package.go create mode 100644 internal/code/package_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2711c47..af561e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,15 +10,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 with: - go-version: 1.17 + go-version: 1.18 - - name: Install dependencies - run: go get -u golang.org/x/lint/golint + - uses: actions/checkout@v3 - name: Build run: go build -v ./... @@ -26,10 +22,8 @@ jobs: - name: Test run: go test -v ./... -covermode=count -coverprofile=cover.out - - name: Vet & Lint - run: | - go vet ./... - golint -set_exit_status ./... + - name: Vet + run: go vet ./... - uses: codecov/codecov-action@v1 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 42f66f5..a59214c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,9 +10,13 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: - version: v1.40.1 + version: v1.50.0 diff --git a/go.mod b/go.mod index af7fac7..8941ada 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,25 @@ module github.com/sunboyy/repogen -go 1.17 +go 1.18 require ( github.com/fatih/camelcase v1.0.0 - go.mongodb.org/mongo-driver v1.9.1 - golang.org/x/tools v0.1.10 + go.mongodb.org/mongo-driver v1.10.3 + golang.org/x/tools v0.1.12 ) require ( - github.com/go-stack/stack v1.8.0 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pkg/errors v0.9.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.0.2 // indirect - github.com/xdg-go/stringprep v1.0.2 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index ed46792..047f388 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= @@ -14,6 +12,7 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -26,56 +25,38 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1c= -go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +go.mongodb.org/mongo-driver v1.10.3 h1:XDQEvmh6z1EUsXuIkXE9TaVeqHw6SwS1uf93jFs0HBA= +go.mongodb.org/mongo-driver v1.10.3/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/code/errors.go b/internal/code/errors.go new file mode 100644 index 0000000..58f0494 --- /dev/null +++ b/internal/code/errors.go @@ -0,0 +1,28 @@ +package code + +import ( + "errors" + "fmt" +) + +var ( + ErrAmbiguousPackageName = errors.New("code: ambiguous package name") +) + +type DuplicateStructError string + +func (err DuplicateStructError) Error() string { + return fmt.Sprintf( + "code: duplicate implementation of struct '%s'", + string(err), + ) +} + +type DuplicateInterfaceError string + +func (err DuplicateInterfaceError) Error() string { + return fmt.Sprintf( + "code: duplicate implementation of interface '%s'", + string(err), + ) +} diff --git a/internal/code/errors_test.go b/internal/code/errors_test.go new file mode 100644 index 0000000..3446807 --- /dev/null +++ b/internal/code/errors_test.go @@ -0,0 +1,36 @@ +package code_test + +import ( + "testing" + + "github.com/sunboyy/repogen/internal/code" +) + +type ErrorTestCase struct { + Name string + Error error + ExpectedString string +} + +func TestError(t *testing.T) { + testTable := []ErrorTestCase{ + { + Name: "DuplicateStructError", + Error: code.DuplicateStructError("User"), + ExpectedString: "code: duplicate implementation of struct 'User'", + }, + { + Name: "DuplicateInterfaceError", + Error: code.DuplicateInterfaceError("UserRepository"), + ExpectedString: "code: duplicate implementation of interface 'UserRepository'", + }, + } + + for _, testCase := range testTable { + t.Run(testCase.Name, func(t *testing.T) { + if testCase.Error.Error() != testCase.ExpectedString { + t.Errorf("Expected = %+v\nReceived = %+v", testCase.ExpectedString, testCase.Error.Error()) + } + }) + } +} diff --git a/internal/code/extractor_test.go b/internal/code/extractor_test.go index 09e3ba3..2b12c33 100644 --- a/internal/code/extractor_test.go +++ b/internal/code/extractor_test.go @@ -64,8 +64,8 @@ type UserModel struct { }`, ExpectedOutput: code.File{ PackageName: "user", - Structs: code.Structs{ - code.Struct{ + Structs: []code.Struct{ + { Name: "UserModel", Fields: code.StructFields{ code.StructField{ @@ -108,8 +108,8 @@ type UserRepository interface { }`, ExpectedOutput: code.File{ PackageName: "user", - Interfaces: code.Interfaces{ - code.InterfaceType{ + Interfaces: []code.InterfaceType{ + { Name: "UserRepository", Methods: []code.Method{ { @@ -229,8 +229,8 @@ type UserRepository interface { {Path: "context"}, {Path: "go.mongodb.org/mongo-driver/bson/primitive"}, }, - Structs: code.Structs{ - code.Struct{ + Structs: []code.Struct{ + { Name: "UserModel", Fields: code.StructFields{ code.StructField{ @@ -252,8 +252,8 @@ type UserRepository interface { }, }, }, - Interfaces: code.Interfaces{ - code.InterfaceType{ + Interfaces: []code.InterfaceType{ + { Name: "UserRepository", Methods: []code.Method{ { diff --git a/internal/code/models.go b/internal/code/models.go index 8397587..17062ed 100644 --- a/internal/code/models.go +++ b/internal/code/models.go @@ -8,8 +8,8 @@ import ( type File struct { PackageName string Imports []Import - Structs Structs - Interfaces Interfaces + Structs []Struct + Interfaces []InterfaceType } // Import is a model for package imports @@ -18,20 +18,6 @@ type Import struct { Path string } -// Structs is a group of Struct model -type Structs []Struct - -// ByName return struct with matching name. Another return value shows whether there is a struct -// with that name exists. -func (strs Structs) ByName(name string) (Struct, bool) { - for _, str := range strs { - if str.Name == name { - return str, true - } - } - return Struct{}, false -} - // Struct is a definition of the struct type Struct struct { Name string @@ -63,20 +49,6 @@ type StructField struct { Tags map[string][]string } -// Interfaces is a group of Interface model -type Interfaces []InterfaceType - -// ByName return interface by name Another return value shows whether there is an interface -// with that name exists. -func (intfs Interfaces) ByName(name string) (InterfaceType, bool) { - for _, intf := range intfs { - if intf.Name == name { - return intf, true - } - } - return InterfaceType{}, false -} - // InterfaceType is a definition of the interface type InterfaceType struct { Name string diff --git a/internal/code/models_test.go b/internal/code/models_test.go index 5c0cb84..2dd734b 100644 --- a/internal/code/models_test.go +++ b/internal/code/models_test.go @@ -7,36 +7,6 @@ import ( "github.com/sunboyy/repogen/internal/code" ) -func TestStructsByName(t *testing.T) { - userStruct := code.Struct{ - Name: "UserModel", - Fields: code.StructFields{ - code.StructField{Name: "ID", Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}}, - code.StructField{Name: "Username", Type: code.SimpleType("string")}, - }, - } - structs := code.Structs{userStruct} - - t.Run("struct found", func(t *testing.T) { - structModel, ok := structs.ByName("UserModel") - - if !ok { - t.Fail() - } - if !reflect.DeepEqual(structModel, userStruct) { - t.Errorf("Expected = %+v\nReceived = %+v", userStruct, structModel) - } - }) - - t.Run("struct not found", func(t *testing.T) { - _, ok := structs.ByName("ProductModel") - - if ok { - t.Fail() - } - }) -} - func TestStructFieldsByName(t *testing.T) { idField := code.StructField{Name: "ID", Type: code.ExternalType{PackageAlias: "primitive", Name: "ObjectID"}} usernameField := code.StructField{Name: "Username", Type: code.SimpleType("string")} @@ -62,30 +32,6 @@ func TestStructFieldsByName(t *testing.T) { }) } -func TestInterfacesByName(t *testing.T) { - userRepoIntf := code.InterfaceType{Name: "UserRepository"} - interfaces := code.Interfaces{userRepoIntf} - - t.Run("struct field found", func(t *testing.T) { - intf, ok := interfaces.ByName("UserRepository") - - if !ok { - t.Fail() - } - if !reflect.DeepEqual(intf, userRepoIntf) { - t.Errorf("Expected = %+v\nReceived = %+v", userRepoIntf, intf) - } - }) - - t.Run("struct field not found", func(t *testing.T) { - _, ok := interfaces.ByName("Password") - - if ok { - t.Fail() - } - }) -} - type TypeCodeTestCase struct { Name string Type code.Type diff --git a/internal/code/package.go b/internal/code/package.go new file mode 100644 index 0000000..3a1e3fa --- /dev/null +++ b/internal/code/package.go @@ -0,0 +1,66 @@ +package code + +import ( + "go/ast" + "strings" +) + +// ParsePackage extracts package name, struct and interface implementations from +// map[string]*ast.Package. Test files will be ignored. +func ParsePackage(pkgs map[string]*ast.Package) (Package, error) { + pkg := NewPackage() + for _, astPkg := range pkgs { + for fileName, file := range astPkg.Files { + if strings.HasSuffix(fileName, "_test.go") { + continue + } + + if err := pkg.addFile(ExtractComponents(file)); err != nil { + return Package{}, err + } + } + } + return pkg, nil +} + +// Package stores package name, struct and interface implementations as a result +// from ParsePackage +type Package struct { + Name string + Structs map[string]Struct + Interfaces map[string]InterfaceType +} + +// NewPackage is a constructor function for Package. +func NewPackage() Package { + return Package{ + Structs: map[string]Struct{}, + Interfaces: map[string]InterfaceType{}, + } +} + +// addFile alters the Package by adding struct and interface implementations in +// the extracted file. If the package name conflicts, it will return error. +func (pkg *Package) addFile(file File) error { + if pkg.Name == "" { + pkg.Name = file.PackageName + } else if pkg.Name != file.PackageName { + return ErrAmbiguousPackageName + } + + for _, structImpl := range file.Structs { + if _, ok := pkg.Structs[structImpl.Name]; ok { + return DuplicateStructError(structImpl.Name) + } + pkg.Structs[structImpl.Name] = structImpl + } + + for _, interfaceImpl := range file.Interfaces { + if _, ok := pkg.Interfaces[interfaceImpl.Name]; ok { + return DuplicateInterfaceError(interfaceImpl.Name) + } + pkg.Interfaces[interfaceImpl.Name] = interfaceImpl + } + + return nil +} diff --git a/internal/code/package_test.go b/internal/code/package_test.go new file mode 100644 index 0000000..fd0527b --- /dev/null +++ b/internal/code/package_test.go @@ -0,0 +1,238 @@ +package code_test + +import ( + "errors" + "go/ast" + "go/parser" + "go/token" + "testing" + + "github.com/sunboyy/repogen/internal/code" +) + +const goImplFile1Data = ` +package codepkgsuccess + +import ( + "math" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Gender string + +const ( + GenderMale Gender = "MALE" + GenderFemale Gender = "FEMALE" +) + +type User struct { + ID primitive.ObjectID ` + "`json:\"id\"`" + ` + Name string ` + "`json:\"name\"`" + ` + Gender Gender ` + "`json:\"gender\"`" + ` + Birthday time.Time ` + "`json:\"birthday\"`" + ` +} + +func (u User) Age() int { + return int(math.Floor(time.Since(u.Birthday).Hours() / 24 / 365)) +} + +type ( + Product struct { + ID primitive.ObjectID ` + "`json:\"id\"`" + ` + Name string ` + "`json:\"name\"`" + ` + Price float64 ` + "`json:\"price\"`" + ` + } + + Order struct { + ID primitive.ObjectID ` + "`json:\"id\"`" + ` + ItemIDs map[primitive.ObjectID]int ` + "`json:\"itemIds\"`" + ` + TotalPrice float64 ` + "`json:\"totalPrice\"`" + ` + UserID primitive.ObjectID ` + "`json:\"userId\"`" + ` + CreatedAt time.Time ` + "`json:\"createdAt\"`" + ` + } +) +` + +const goImplFile2Data = ` +package codepkgsuccess + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type OrderService interface { + CreateOrder(u User, products map[Product]int) Order +} + +type OrderServiceImpl struct{} + +func (s *OrderServiceImpl) CreateOrder(u User, products map[Product]int) Order { + itemIDs := map[primitive.ObjectID]int{} + var totalPrice float64 + for product, amount := range products { + itemIDs[product.ID] = amount + totalPrice += product.Price * float64(amount) + } + + return Order{ + ID: primitive.NewObjectID(), + ItemIDs: map[primitive.ObjectID]int{}, + TotalPrice: totalPrice, + UserID: u.ID, + CreatedAt: time.Now(), + } +} +` + +const goImplFile3Data = ` +package success +` + +const goImplFile4Data = ` +package codepkgsuccess + +type User struct { + Name string +} +` + +const goImplFile5Data = ` +package codepkgsuccess + +import "go.mongodb.org/mongo-driver/bson/primitive" + +type OrderService interface { + CancelOrder(orderID primitive.ObjectID) error +} +` + +const goTestFileData = ` +package codepkgsuccess + +type TestCase struct { + Name string + Params []interface{} + Expected string + Actual string +} +` + +var ( + goImplFile1 *ast.File + goImplFile2 *ast.File + goImplFile3 *ast.File + goImplFile4 *ast.File + goImplFile5 *ast.File + goTestFile *ast.File +) + +func init() { + fset := token.NewFileSet() + goImplFile1, _ = parser.ParseFile(fset, "", goImplFile1Data, parser.ParseComments) + goImplFile2, _ = parser.ParseFile(fset, "", goImplFile2Data, parser.ParseComments) + goImplFile3, _ = parser.ParseFile(fset, "", goImplFile3Data, parser.ParseComments) + goImplFile4, _ = parser.ParseFile(fset, "", goImplFile4Data, parser.ParseComments) + goImplFile5, _ = parser.ParseFile(fset, "", goImplFile5Data, parser.ParseComments) + goTestFile, _ = parser.ParseFile(fset, "", goTestFileData, parser.ParseComments) +} + +func TestParsePackage_Success(t *testing.T) { + pkg, err := code.ParsePackage(map[string]*ast.Package{ + "codepkgsuccess": { + Files: map[string]*ast.File{ + "file1.go": goImplFile1, + "file2.go": goImplFile2, + "file1_test.go": goTestFile, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + if pkg.Name != "codepkgsuccess" { + t.Errorf("expected package name 'codepkgsuccess', got '%s'", pkg.Name) + } + if _, ok := pkg.Structs["User"]; !ok { + t.Error("struct 'User' not found") + } + if _, ok := pkg.Structs["Product"]; !ok { + t.Error("struct 'Product' not found") + } + if _, ok := pkg.Structs["Order"]; !ok { + t.Error("struct 'Order' not found") + } + if _, ok := pkg.Structs["OrderServiceImpl"]; !ok { + t.Error("struct 'OrderServiceImpl' not found") + } + if _, ok := pkg.Interfaces["OrderService"]; !ok { + t.Error("interface 'OrderService' not found") + } + if _, ok := pkg.Structs["TestCase"]; ok { + t.Error("unexpected struct 'TestCase' in test file") + } +} + +func TestParsePackage_AmbiguousPackageName(t *testing.T) { + _, err := code.ParsePackage(map[string]*ast.Package{ + "codepkgsuccess": { + Files: map[string]*ast.File{ + "file1.go": goImplFile1, + "file2.go": goImplFile2, + "file3.go": goImplFile3, + }, + }, + }) + + if !errors.Is(err, code.ErrAmbiguousPackageName) { + t.Errorf( + "expected error '%s', got '%s'", + code.ErrAmbiguousPackageName.Error(), + err.Error(), + ) + } +} + +func TestParsePackage_DuplicateStructs(t *testing.T) { + _, err := code.ParsePackage(map[string]*ast.Package{ + "codepkgsuccess": { + Files: map[string]*ast.File{ + "file1.go": goImplFile1, + "file2.go": goImplFile2, + "file4.go": goImplFile4, + }, + }, + }) + + if !errors.Is(err, code.DuplicateStructError("User")) { + t.Errorf( + "expected error '%s', got '%s'", + code.ErrAmbiguousPackageName.Error(), + err.Error(), + ) + } +} + +func TestParsePackage_DuplicateInterfaces(t *testing.T) { + _, err := code.ParsePackage(map[string]*ast.Package{ + "codepkgsuccess": { + Files: map[string]*ast.File{ + "file1.go": goImplFile1, + "file2.go": goImplFile2, + "file5.go": goImplFile5, + }, + }, + }) + + if !errors.Is(err, code.DuplicateInterfaceError("OrderService")) { + t.Errorf( + "expected error '%s', got '%s'", + code.ErrAmbiguousPackageName.Error(), + err.Error(), + ) + } +} diff --git a/internal/spec/field.go b/internal/spec/field.go index 2532cba..4d52726 100644 --- a/internal/spec/field.go +++ b/internal/spec/field.go @@ -24,7 +24,7 @@ func (r FieldReference) ReferencingCode() string { } type fieldResolver struct { - Structs code.Structs + Structs map[string]code.Struct } func (r fieldResolver) ResolveStructField(structModel code.Struct, tokens []string) (FieldReference, bool) { @@ -46,7 +46,7 @@ func (r fieldResolver) ResolveStructField(structModel code.Struct, tokens []stri continue } - childStruct, ok := r.Structs.ByName(fieldSimpleType.Code()) + childStruct, ok := r.Structs[fieldSimpleType.Code()] if !ok { continue } diff --git a/internal/spec/parser.go b/internal/spec/parser.go index d04d414..88f0dca 100644 --- a/internal/spec/parser.go +++ b/internal/spec/parser.go @@ -6,7 +6,7 @@ import ( ) // ParseInterfaceMethod returns repository method spec from declared interface method -func ParseInterfaceMethod(structs code.Structs, structModel code.Struct, method code.Method) (MethodSpec, error) { +func ParseInterfaceMethod(structs map[string]code.Struct, structModel code.Struct, method code.Method) (MethodSpec, error) { parser := interfaceMethodParser{ fieldResolver: fieldResolver{ Structs: structs, diff --git a/internal/spec/parser_test.go b/internal/spec/parser_test.go index 1bbd58d..d49ab2a 100644 --- a/internal/spec/parser_test.go +++ b/internal/spec/parser_test.go @@ -91,9 +91,9 @@ var ( } ) -var structs = code.Structs{ - nameStruct, - structModel, +var structs = map[string]code.Struct{ + nameStruct.Name: nameStruct, + structModel.Name: structModel, } type ParseInterfaceMethodTestCase struct { diff --git a/main.go b/main.go index 313046a..feb976f 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,7 @@ func main() { flag.Usage = printUsage versionPtr := flag.Bool("version", false, "print version of repogen") - sourcePtr := flag.String("src", "", "source file") + pkgDirPtr := flag.String("pkg", ".", "package directory to scan for model struct and repository interface") destPtr := flag.String("dest", "", "destination file") modelPtr := flag.String("model", "", "model struct name") repoPtr := flag.String("repo", "", "repository interface name") @@ -39,10 +39,6 @@ func main() { return } - if *sourcePtr == "" { - printUsage() - log.Fatal("-src flag required") - } if *modelPtr == "" { printUsage() log.Fatal("-model flag required") @@ -52,7 +48,7 @@ func main() { log.Fatal("-repo flag required") } - code, err := generateFromRequest(*sourcePtr, *modelPtr, *repoPtr) + code, err := generateFromRequest(*pkgDirPtr, *modelPtr, *repoPtr) if err != nil { panic(err) } @@ -84,33 +80,40 @@ func printVersion() { fmt.Println(version) } -func generateFromRequest(fileName, structModelName, repositoryInterfaceName string) (string, error) { +func generateFromRequest(pkgDir, structModelName, repositoryInterfaceName string) (string, error) { fset := token.NewFileSet() - f, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments) + dir, err := parser.ParseDir(fset, pkgDir, nil, parser.ParseComments) if err != nil { - panic(err) + return "", err } - file := code.ExtractComponents(f) + pkg, err := code.ParsePackage(dir) + if err != nil { + return "", err + } - structModel, ok := file.Structs.ByName(structModelName) + return generateRepository(pkg, structModelName, repositoryInterfaceName) +} + +func generateRepository(pkg code.Package, structModelName, repositoryInterfaceName string) (string, error) { + structModel, ok := pkg.Structs[structModelName] if !ok { return "", errors.New("struct model not found") } - intf, ok := file.Interfaces.ByName(repositoryInterfaceName) + intf, ok := pkg.Interfaces[repositoryInterfaceName] if !ok { return "", errors.New("interface model not found") } var methodSpecs []spec.MethodSpec for _, method := range intf.Methods { - methodSpec, err := spec.ParseInterfaceMethod(file.Structs, structModel, method) + methodSpec, err := spec.ParseInterfaceMethod(pkg.Structs, structModel, method) if err != nil { return "", err } methodSpecs = append(methodSpecs, methodSpec) } - return generator.GenerateRepository(file.PackageName, structModel, intf.Name, methodSpecs) + return generator.GenerateRepository(pkg.Name, structModel, intf.Name, methodSpecs) }