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
54 changes: 54 additions & 0 deletions .github/workflows/go-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Go Test

on:
pull_request:
branches: [ "main" ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: true
cache-dependency-path: |
go.sum
_test/testdata/go.sum

- name: Download dependencies
run: |
find . -name go.mod -not -path '*/vendor/*' -execdir go mod download \;

- name: Verify modules
run: |
go mod tidy
git diff --exit-code -- go.mod go.sum || (echo "Run 'go mod tidy' locally" && exit 1)

- name: Format
run: |
go fmt ./...
git diff --exit-code || (echo "Run 'go fmt ./...' locally" && exit 1)

- name: Vet
run: go vet ./...

- name: Test
run: go test -race ./...

- name: Build
run: |
go build -trimpath -ldflags '-s -w' .
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

BIN := pflagstruct
OUTDIR := bin
GO := go
LDFLAGS := -s -w

.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Build

.PHONY: build
build: ## Compiles the source code.
$(GO) build -trimpath -ldflags '$(LDFLAGS)' -o $(OUTDIR)/$(BIN) .

.PHONY: test
test: ## Run all unit tests.
$(GO) test ./...

.PHONY: install
install: ## Build and install the binary
$(GO) install -trimpath -ldflags '$(LDFLAGS)' .
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ The CLI provides the following flags:
Feel free to explore the available flags and experiment with different options to generate code based on your struct
definitions.

## How the Project Is Built

The project is structured into modular internal packages, each with a specific responsibility:

* `internal/scan/` → scans Go code for struct definitions, fields, packages, and projects.
* `internal/code/` → generates Go code ([`jen` library](https://github.com/dave/jennifer) is used to programmatically build Go ASTs).
* `internal/dir/` → handles filesystem paths.
* `projscan/` → defines core types (`Struct`, `Field`, `Package`, etc.) used in scanning and generation.
* `main.go` → CLI entrypoint, built with `cobra`.

## Contributing

Contributions to the pflagstruct are welcome! If you find any issues or have suggestions for improvement,
Expand Down
9 changes: 5 additions & 4 deletions _test/testdata/bar/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import (
)

type Quux struct {
Id string `json:"Id"`
Name string `json:"Name"`
Quuz Quuz `json:"Quuz"`
Status string `json:"Status"`
Id string `json:"Id"`
Name string `json:"Name"`
Quuz Quuz `json:"Quuz"`
Status string `json:"Status"`
Numbers *[]int `json:"Numbers"`
}

type Quuz struct {
Expand Down
2 changes: 1 addition & 1 deletion _test/testdata/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cloud104/pflagstruct/_test/testdata

go 1.20
go 1.25.0

require github.com/apirator/apirator v1.0.1

Expand Down
25 changes: 13 additions & 12 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
module github.com/cloud104/pflagstruct

go 1.20
go 1.25.0

require (
github.com/dave/jennifer v1.6.1
github.com/dave/jennifer v1.7.1
github.com/enescakir/emoji v1.0.0
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/gookit/color v1.5.3
github.com/gookit/color v1.6.0
github.com/ku/go-change-case v0.0.1
github.com/lmittmann/tint v0.3.4
github.com/lmittmann/tint v1.1.2
github.com/pkg/errors v0.9.1
github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
github.com/wk8/go-ordered-map/v2 v2.1.7
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/mod v0.11.0
github.com/samber/lo v1.51.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.1
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/mod v0.27.0
)

require (
Expand All @@ -25,8 +24,10 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
50 changes: 27 additions & 23 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,56 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/ku/go-change-case v0.0.1 h1:Dym6yulm/ma+XRgH1SG2CIOLk2biWpSFf6mMPtg3ic8=
github.com/ku/go-change-case v0.0.1/go.mod h1:7rNOPNKPrCdjm8+iDIKgs87tZuyPJkhnStQZIOT3Pss=
github.com/lmittmann/tint v0.3.4 h1:QOr2U9GKQfNsNhKPhL7PexQm0mqkRmvuy1UrZb6AidM=
github.com/lmittmann/tint v0.3.4/go.mod h1:vYasuAV5qbz2TYeUK+sj8iURGIl9T/WOlh4qzYGP16I=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/wk8/go-ordered-map/v2 v2.1.7 h1:aUZ1xBMdbvY8wnNt77qqo4nyT3y0pX4Usat48Vm+hik=
github.com/wk8/go-ordered-map/v2 v2.1.7/go.mod h1:9Xvgm2mV2kSq2SAm0Y608tBmu8akTzI7c2bz7/G7ZN4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
23 changes: 23 additions & 0 deletions internal/goenv/getter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package goenv

import (
"os/exec"
"strings"

"github.com/pkg/errors"
)

// Get retrieves the value of a Go environment variable
func Get(key string) (string, error) {
out, err := exec.Command("go", "env", key).Output()
if err != nil {
return "", errors.Wrapf(err, "failed to get Go env variable %q", key)
}

value := strings.TrimSpace(string(out))
if value == "" {
return "", errors.Errorf("Go env variable %q is empty", key)
}

return value, nil
}
42 changes: 42 additions & 0 deletions internal/goenv/getter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package goenv

import "testing"

func TestGet(t *testing.T) {
type args struct {
key string
}

tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "valid GOVERSION",
args: args{key: "GOVERSION"},
want: "go1.25.0",
wantErr: false,
},
{
name: "invalid key returns error",
args: args{key: "__THIS_GO_ENV_KEY_DOES_NOT_EXIST__"},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Get(tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr && got != tt.want {
t.Errorf("Get() got = %v, want %v", got, tt.want)
}
})
}
}
25 changes: 25 additions & 0 deletions internal/goenv/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package goenv

import (
"bytes"
"os/exec"
"strings"

"github.com/pkg/errors"
)

// ListDir runs "go list -f '{{.Dir}}' <pkg>" and returns the source path.
func ListDir(pkg string) (string, error) {
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", pkg)

var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out

err := cmd.Run()
if err != nil {
return "", errors.Errorf("failed to run go list: %v, output: %s", err, out.String())
}

return strings.TrimSpace(out.String()), nil
}
50 changes: 50 additions & 0 deletions internal/goenv/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package goenv

import (
path "path"
"testing"
)

func TestGoListDir(t *testing.T) {
goroot, err := Get("GOROOT")
if err != nil {
t.Fatalf("failed to retrieve GOROOT using go env: %v", err)
}

type args struct {
pkg string
}

tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "stdlib package fmt",
args: args{pkg: "fmt"},
want: path.Join(goroot, "src/fmt"),
wantErr: false,
},
{
name: "nonexistent package returns error",
args: args{pkg: "not/a/real/package/definitelynot"},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ListDir(tt.args.pkg)
if (err != nil) != tt.wantErr {
t.Errorf("ListDir() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr && got != tt.want {
t.Errorf("ListDir() got = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion internal/scan/fld/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (f *Finder) buildField(expr ast.Expr, st *projscan.Struct, proj *projscan.P
Type: field.Type,
Doc: field.Doc,
StructRef: field.StructRef,
Pointer: false,
Pointer: field.Pointer,
Array: true,
ArrayPointer: field.Pointer,
})
Expand Down
Loading