├── .github └── workflows │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── main.go /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - "!*" 6 | tags: 7 | - "v*.*.*" 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: goreleaser 12 | steps: 13 | - name: Set up Go 1.13 14 | uses: actions/setup-go@v1 15 | with: 16 | go-version: 1.13 17 | id: go 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v1 20 | - name: Check GITHUB_REF environment variable value 21 | run: echo $GITHUB_REF 22 | - name: Release via goreleaser 23 | uses: goreleaser/goreleaser-action@master 24 | with: 25 | args: release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eli Yukelzon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # struct2interface 2 | 3 | struct2interface is a CLI utility to extract an interface from a Golang struct 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/reflog/struct2interface 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```bash 14 | struct2interface --help 15 | 16 | Usage: 17 | struct2interface [flags] 18 | 19 | Flags: 20 | -f, --folder string Path to the package in which the struct resides 21 | -h, --help help for struct2interface 22 | -i, --interface string Name of the output interface 23 | -o, --output string Path to output file (will be overwritten) 24 | -p, --package string Name of the package in which the struct resides 25 | -s, --struct string Name of the input struct 26 | -t, --template string Path to a Go template file to use for writing the resulting interface 27 | 28 | struct2interface -f "/home/reflog/go/src/github.com/mattermost/mattermost-server/app" -o "/home/reflog/go/src/github.com/mattermost/mattermost-server/ 29 | app/app_iface.go" -p "app" -s "App" -i "AppIface" 30 | ``` 31 | 32 | ## Other tools 33 | Before writing this, I tried the following projects, but encountered issues: 34 | 35 | [Interfacer](https://github.com/rjeczalik/interfaces) - incredibly slow, dumps odd messages to stderr and writes fully qualified package name instead of localized one, i.e. `*github.com/mattermost/mattermost-server/v5/model.Config` instead of `*model.Config` 36 | [Ifacemaker](https://github.com/vburenin/ifacemaker) - created duplicate imports (in my case `"html/template"` and `"text/template"`) 37 | 38 | ## Contributing 39 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 40 | 41 | Please make sure to update tests as appropriate. 42 | 43 | ## License 44 | [MIT](https://choosealicense.com/licenses/mit/) 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/reflog/struct2interface 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/spf13/cobra v0.0.5 7 | golang.org/x/tools v0.0.0-20200206050830-dd0d5d485177 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 10 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 11 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 12 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 13 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 14 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 17 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 18 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 19 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 20 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 21 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 22 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 23 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 24 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 25 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 26 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 27 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 28 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 31 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= 32 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 33 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 34 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 35 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 38 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/tools v0.0.0-20200206050830-dd0d5d485177 h1:E2vxBajJgSA3TcJhDGTh/kP3VnsvXKl9jSijv+h7svQ= 41 | golang.org/x/tools v0.0.0-20200206050830-dd0d5d485177/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 42 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 43 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 46 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 | // See LICENSE.txt for license information. 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/ast" 10 | "go/parser" 11 | "go/printer" 12 | "go/token" 13 | "io/ioutil" 14 | "log" 15 | "os" 16 | "sort" 17 | "strings" 18 | "text/template" 19 | 20 | "github.com/spf13/cobra" 21 | "golang.org/x/tools/imports" 22 | ) 23 | 24 | const DEFAULT_TEMPLATE = ` 25 | // DO NOT EDIT, auto generated by struct2interface 26 | 27 | package {{.Package}} 28 | 29 | type {{.Name}} interface { 30 | {{.Content}} 31 | } 32 | ` 33 | 34 | func functionDef(fun *ast.FuncDecl, fset *token.FileSet) string { 35 | name := fun.Name.Name 36 | params := make([]string, 0) 37 | for _, p := range fun.Type.Params.List { 38 | var typeNameBuf bytes.Buffer 39 | err := printer.Fprint(&typeNameBuf, fset, p.Type) 40 | if err != nil { 41 | log.Fatalf("failed printing %s", err) 42 | } 43 | names := make([]string, 0) 44 | for _, name := range p.Names { 45 | names = append(names, name.Name) 46 | } 47 | params = append(params, fmt.Sprintf("%s %s", strings.Join(names, ","), typeNameBuf.String())) 48 | } 49 | returns := make([]string, 0) 50 | if fun.Type.Results != nil { 51 | for _, r := range fun.Type.Results.List { 52 | var typeNameBuf bytes.Buffer 53 | err := printer.Fprint(&typeNameBuf, fset, r.Type) 54 | if err != nil { 55 | log.Fatalf("failed printing %s", err) 56 | } 57 | 58 | returns = append(returns, typeNameBuf.String()) 59 | } 60 | } 61 | returnString := "" 62 | if len(returns) == 1 { 63 | returnString = returns[0] 64 | } else if len(returns) > 1 { 65 | returnString = fmt.Sprintf("(%s)", strings.Join(returns, ", ")) 66 | } 67 | return fmt.Sprintf("%s (%s) %v", name, strings.Join(params, ", "), returnString) 68 | } 69 | 70 | func generateInterface(folder, outputFile, pkgName, structName, ifName, outputTemplate string) { 71 | fset := token.NewFileSet() 72 | 73 | pkgs, err := parser.ParseDir(fset, folder, nil, parser.AllErrors) 74 | if err != nil { 75 | log.Fatalf("Unable to parse %s folder", folder) 76 | } 77 | var appPkg *ast.Package 78 | for _, pkg := range pkgs { 79 | if pkg.Name == pkgName { 80 | appPkg = pkg 81 | break 82 | } 83 | } 84 | if appPkg == nil { 85 | log.Fatalf("Unable to find package %s", pkgName) 86 | } 87 | 88 | funcs := make([]string, 0) 89 | for _, file := range appPkg.Files { 90 | log.Printf("parsing %s\n", fset.File(file.Pos()).Name()) 91 | if fset.File(file.Pos()).Name() == outputFile { 92 | continue 93 | } 94 | ast.Inspect(file, func(n ast.Node) bool { 95 | if fun, ok := n.(*ast.FuncDecl); ok { 96 | if fun.Recv != nil { 97 | if fun.Name.IsExported() { 98 | if fun.Recv != nil && len(fun.Recv.List) == 1 { 99 | if r, rok := fun.Recv.List[0].Type.(*ast.StarExpr); rok && r.X.(*ast.Ident).Name == structName { 100 | funcs = append(funcs, functionDef(fun, fset)) 101 | } 102 | } 103 | } 104 | } 105 | 106 | } 107 | return true 108 | }) 109 | } 110 | sort.Strings(funcs) 111 | out := bytes.NewBufferString("") 112 | 113 | t := template.Must(template.New("").Parse(outputTemplate)) 114 | err = t.Execute(out, map[string]interface{}{ 115 | "Content": strings.Join(funcs, "\n"), 116 | "Name": ifName, 117 | "Package": pkgName, 118 | }) 119 | if err != nil { 120 | log.Panic(err) 121 | } 122 | os.Remove(outputFile) 123 | formatted, err := imports.Process(outputFile, out.Bytes(), &imports.Options{Comments: true}) 124 | if err != nil { 125 | log.Panic(err) 126 | } 127 | err = ioutil.WriteFile(outputFile, formatted, 0644) 128 | if err != nil { 129 | log.Panic(err) 130 | } 131 | log.Printf("Written %s successfully", outputFile) 132 | } 133 | 134 | func main() { 135 | 136 | var folder, outputFile, pkgName, structName, ifName, outputTemplateFile string 137 | outputTemplate := DEFAULT_TEMPLATE 138 | 139 | var rootCmd = &cobra.Command{ 140 | Use: "struct2interface", 141 | Short: "Extract an interface from a Golang struct", 142 | Run: func(cmd *cobra.Command, args []string) { 143 | 144 | if outputTemplateFile != "" { 145 | d, err := ioutil.ReadFile(outputTemplateFile) 146 | if err != nil { 147 | log.Panic(err) 148 | } 149 | outputTemplate = string(d) 150 | } 151 | 152 | generateInterface(folder, outputFile, pkgName, structName, ifName, outputTemplate) 153 | }, 154 | } 155 | 156 | rootCmd.Flags().StringVarP(&folder, "folder", "f", "", "Path to the package in which the struct resides") 157 | _ = rootCmd.MarkFlagRequired("folder") 158 | rootCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Path to output file (will be overwritten)") 159 | _ = rootCmd.MarkFlagRequired("output") 160 | rootCmd.Flags().StringVarP(&pkgName, "package", "p", "", "Name of the package in which the struct resides") 161 | _ = rootCmd.MarkFlagRequired("package") 162 | rootCmd.Flags().StringVarP(&structName, "struct", "s", "", "Name of the input struct") 163 | _ = rootCmd.MarkFlagRequired("struct") 164 | rootCmd.Flags().StringVarP(&ifName, "interface", "i", "", "Name of the output interface") 165 | _ = rootCmd.MarkFlagRequired("interface") 166 | rootCmd.Flags().StringVarP(&outputTemplateFile, "template", "t", "", "Path to a Go template file to use for writing the resulting interface") 167 | 168 | if err := rootCmd.Execute(); err != nil { 169 | fmt.Println(err) 170 | os.Exit(1) 171 | } 172 | 173 | } 174 | --------------------------------------------------------------------------------