├── .github └── workflows │ ├── go.yml │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── ascii.txt ├── assets └── misura_wallpaper.png ├── config └── config.go ├── examples └── readme_usage │ ├── main.go │ └── main.misura.go ├── go.mod ├── go.sum ├── main.go ├── pre-commit.sh ├── templates ├── header.gotmpl ├── interface_decl.gotmpl ├── interface_decl_comma.gotmpl ├── version.gotmpl └── wrapper.gotmpl └── wrapper ├── comment_visitor.go ├── comment_visitor_test.go ├── generator.go ├── helpers.go ├── test_samples ├── magic_comment.go ├── mytime │ └── mytime.go ├── test.go └── time.go ├── types ├── func_param.go ├── method.go └── target.go ├── visitor.go └── wrapper_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.22' 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: '>=1.21' 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.misura.go 2 | dist/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | - go mod tidy 14 | builds: 15 | - env: 16 | - CGO_ENABLED=0 17 | goos: 18 | - linux 19 | - windows 20 | - darwin 21 | ldflags: 22 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser -X main.website=https://sinashabani.dev" 23 | archives: 24 | - format: tar.gz 25 | # this name template makes the OS and Arch compatible with the results of `uname`. 26 | name_template: >- 27 | {{ .ProjectName }}_ 28 | {{- title .Os }}_ 29 | {{- if eq .Arch "amd64" }}x86_64 30 | {{- else if eq .Arch "386" }}i386 31 | {{- else }}{{ .Arch }}{{ end }} 32 | {{- if .Arm }}v{{ .Arm }}{{ end }} 33 | # use zip for windows archives 34 | format_overrides: 35 | - goos: windows 36 | format: zip 37 | 38 | changelog: 39 | sort: asc 40 | filters: 41 | exclude: 42 | - "^docs:" 43 | - "^test:" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2023 Sina Shabani 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Misura 2 | 3 | > Misura (Italian for measure) gives insight about a golang type by generating a wrapper. See [usage](#usage) seciton for more information 4 | 5 | ![Misura Wallpaper](./assets/misura_wallpaper.png) 6 | 7 |
8 | ⚠️⚠️ Misura IS UNDER HEAVY DEVELOPMENT ⚠️⚠️ 9 |
10 |
11 | 12 | ## Features 13 | 14 | * Can wrap any interface not matter the input or output 15 | * it's quite versatile by receiving a `metrics` interface in the form of: 16 | ```golang 17 | interface { 18 | Failure(ctx context.Context, name, pkg, intr, method string, duration time.Duration, err error) 19 | Success(ctx context.Context, name, pkg, intr, method string, duration time.Duration) 20 | Total(ctx context.Context, name, pkg, intr, method string) 21 | } 22 | ``` 23 | * You can use it to easily add Prometheus metrics to any interface you want or enable tracing (i.e. [opentracing](https://github.com/opentracing/opentracing-go)) without cluttering the actual logic. 24 | * It's smart enough not to polute git changes everytime `go generate` is ran. 25 | * TODO 26 | 27 | ## Installation 28 | 29 | * Latest version of Misura can be obtained from the [Rleases](https://github.com/itzloop/misura/releases). 30 | 31 | * Misura can also be installed with `go install` command: 32 | 33 | ```bash 34 | $ go install github.com/itzloop/misura@latest 35 | ``` 36 | 37 | ## Usage 38 | 39 | Assume we have these interfaces in a file called `sample.go`. 40 | 41 | ```golang 42 | // filename: sample.go 43 | type FooType interface { 44 | Foo(int, string) error 45 | } 46 | 47 | type BarType interface { 48 | Bar() (string, error) 49 | Baz(string) 50 | } 51 | ``` 52 | 53 | Now to generate a wrapper for this interface we have 2 options: 54 | 55 | 1. Put a magic comment for the entire file and passing each interface name with the -t flag. 56 | 57 | ```golang 58 | // filename: sample.go 59 | //go:generate misura -m all -t FooType -t BarType 60 | type FooType interface { 61 | Foo(int, string) error 62 | } 63 | 64 | type BarType interface { 65 | Bar() (string, error) 66 | Baz(string) 67 | } 68 | ``` 69 | 70 | ### Explaination 71 | 72 | * `-m` receives specifies what to measure. `all` measures everything (total calls, error and success count and the duration of that call). 73 | * You can repeadedly pass `-m` like `-m total -m duration` or pass it as a comma-seperated string like `-m total,duration`. 74 | * When `all` is passed other measures will be ignored. 75 | * `-t` specifies what types to generate a wrapper for. 76 | * `-t` also supports comma-seperated and repeated or both. 77 | 78 | 2. Add a magic comment on top of the file then use `//misura:` syntax. This method makes the file readable. 79 | 80 | ```golang 81 | // filename: sample.go 82 | package main 83 | 84 | // passed args will be used for all types 85 | // right now having per-type args is NOT supported! 86 | //go:generate misura -m all 87 | import ( 88 | ... 89 | ) 90 | 91 | //misura:FooType 92 | type FooType interface { 93 | Foo(int, string) error 94 | } 95 | 96 | //misura:BarType 97 | type BarType interface { 98 | Bar() (string, error) 99 | Baz(string) 100 | } 101 | ``` 102 | 103 | The second approach is more readable as it keeps comments close to the actual type istead of having a single comment. 104 | 105 | After running `go generate ./...` on the above file, two files will be generated: 106 | 1. `sample.FooType.misura.go` 107 | 2. `sample.BarType.misura.go` 108 | 109 | Here is the contents of the first file: 110 | ``` 111 | // Code generated by github.com/itzloop/misura. DO NOT EDIT! 112 | // RANDOM_HEX=69679DA8 113 | // This is used to avoid name colision. as start and duration are common names. 114 | ... 115 | type FooTypePrometheusWrapperImpl struct { 116 | name string 117 | intr string 118 | wrapped FooType 119 | metrics interface { 120 | Failure(ctx context.Context, name, pkg, intr, method string, duration time.Duration, err error) 121 | Success(ctx context.Context, name, pkg, intr, method string, duration time.Duration) 122 | Total(ctx context.Context, name, pkg, intr, method string) 123 | } 124 | } 125 | 126 | func NewFooTypePrometheusWrapperImpl(/* removed for clarity */) *FooTypePrometheusWrapperImpl { 127 | // constructor logic, removed for clarity. 128 | } 129 | 130 | func (w *FooTypePrometheusWrapperImpl) Foo(a int, b string) error { 131 | start69679DA8 := time.Now() 132 | w.metrics.Total(context.Background(), w.name, "main", w.intr, "Foo") 133 | err := w.wrapped.Foo(a, b) 134 | duration69679DA8 := time.Since(start69679DA8) 135 | if err != nil { 136 | w.metrics.Failure(context.Background(), w.name, "main", w.intr, "Foo", duration69679DA8, err) 137 | return err 138 | } 139 | w.metrics.Success(context.Background(), w.name, "main", w.intr, "Foo", duration69679DA8) 140 | 141 | return err 142 | } 143 | ``` 144 | 145 | ## Testing 146 | 147 | ```bash 148 | go test ./... 149 | ``` 150 | 151 | - [x] Test generated wrappers for compliation 152 | - [ ] ~~Test generated code is as expected using `ast`, or maybe run them with a utilty program and run it that way.~~ 153 | This is too much for now 154 | 155 | ## Todos 156 | 157 | - [x] Handle slice ... operator 158 | - [x] Only work on types passed not all interfaces in file 159 | - [x] Pass method name and other method related information Total, Success and Error 160 | - [x] Get list of types not just one 161 | - [ ] ~~Check generated file exists, if yes append to it.~~ 162 | - [x] Create a seperate file for each type. 163 | - [x] Let users decided what metrics they want 164 | - [ ] Handle `time` package conflict 165 | - [ ] Add struct wrapping support? 166 | - Only methods in the same file will be included 167 | - Add an options to create an interface for the struct aswell 168 | - [ ] Enable users to extend wrapping functionallity to add custom logic to their interfaces 169 | - [x] ~~Custom metrics?~~ This is solved by accepting metrics interface. 170 | - [ ] Per type method inclusion and exlusion 171 | - [x] Support both go:generate misura [args] and //misura: [args] 172 | - [ ] Support per type args with //misura: 173 | - [ ] Support third party types 174 | - [ ] Rename metrics with measures 175 | - [ ] Rename targets with types 176 | - [ ] Change `PrometheusWrapper` strings with `MisuraWrapper`. 177 | 178 | ## Contrubuting 179 | 180 | TODO 181 | -------------------------------------------------------------------------------- /ascii.txt: -------------------------------------------------------------------------------- 1 | _ 2 | /\/\ (_)___ _ _ _ __ __ _ 3 | / \| / __| | | | '__/ _` | 4 | / /\/\ \ \__ \ |_| | | | (_| | 5 | \/ \/_|___/\__,_|_| \__,_| 6 | -------------------------------------------------------------------------------- /assets/misura_wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzloop/misura/0b6046bf5a572dd255aa3632a09777b06afa0025/assets/misura_wallpaper.png -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "strings" 6 | ) 7 | 8 | type Measures []string 9 | 10 | func (m *Measures) String() string { 11 | return "[" + strings.Join(*m, ", ") + "]" 12 | } 13 | 14 | func (i *Measures) Set(v string) error { 15 | if v == "all" { 16 | *i = []string{"all"} 17 | return nil 18 | } 19 | 20 | for _, it := range *i { 21 | // if has all option don't add anything else 22 | if it == "all" { 23 | *i = []string{"all"} 24 | return nil 25 | } 26 | if it == v { 27 | // don't add dups 28 | return nil 29 | } 30 | } 31 | 32 | if strings.Contains(v, ",") { 33 | if strings.Contains(v, "all") { 34 | *i = []string{"all"} 35 | return nil 36 | } 37 | *i = append(*i, strings.Split(v, ",")...) 38 | return nil 39 | } 40 | 41 | *i = append(*i, v) 42 | return nil 43 | } 44 | 45 | type Types []string 46 | 47 | func (t *Types) String() string { 48 | return "[" + strings.Join(*t, ", ") + "]" 49 | } 50 | 51 | func (t *Types) Set(v string) error { 52 | if strings.Contains(v, ",") { 53 | *t = append(*t, strings.Split(v, ",")...) 54 | return nil 55 | } 56 | 57 | *t = append(*t, v) 58 | return nil 59 | } 60 | 61 | type Config struct { 62 | FormatImports *bool 63 | ShowVersion *bool 64 | Types *Types 65 | Measures *Measures 66 | FilePath *string 67 | 68 | flagSet *flag.FlagSet 69 | } 70 | 71 | func NewConfig(name string) *Config { 72 | cfg := &Config{ 73 | FormatImports: new(bool), 74 | ShowVersion: new(bool), 75 | Types: new(Types), 76 | Measures: new(Measures), 77 | FilePath: new(string), 78 | // TODO: does this need to be more configurable? 79 | flagSet: flag.NewFlagSet(name, flag.ExitOnError), 80 | } 81 | 82 | // should accept multipe targets 83 | cfg.flagSet.Var(cfg.Types, "t", `List of target interface(s)/struct(s). 84 | Can be repeated like '-t MyInterface -t MyStruct' 85 | Can also be comma seprated like '-t MyInterface,MyStruct'`) 86 | 87 | cfg.flagSet.Var(cfg.Measures, "m", `Measures to include. 88 | Possible values [all, duration, total, success, error]. 89 | Can be reapeted like '-m duration -m total' 90 | Can also be comma seprated '-m duration,total' 91 | If 'all' is specified others will be ignored`) 92 | 93 | cfg.flagSet.BoolVar(cfg.FormatImports, "fmt", true, "If set to true, will run imports.Process on the generated wrapper") 94 | cfg.flagSet.BoolVar(cfg.ShowVersion, "version", false, "Show program version") 95 | cfg.flagSet.BoolVar(cfg.ShowVersion, "v", false, "Show program version") 96 | cfg.flagSet.StringVar(cfg.FilePath, "f", "", "File path to parse. Can be overwritten with GOFILE") 97 | 98 | return cfg 99 | } 100 | 101 | func (c *Config) Parse(args []string) error { 102 | return c.flagSet.Parse(args) 103 | } 104 | -------------------------------------------------------------------------------- /examples/readme_usage/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net" 10 | "net/http" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | ) 16 | 17 | // TODO find a better way to call with magic comment 18 | // TODO either by putting a binary in PATH or sth else 19 | // 20 | //go:generate misura 21 | 22 | //misura:IPUtil 23 | type IPUtil interface { 24 | PublicIP() (net.IP, error) 25 | LocalIPs() ([]net.IP, error) 26 | } 27 | 28 | type IPUtilImpl struct { 29 | } 30 | 31 | func (u *IPUtilImpl) PublicIP() (net.IP, error) { 32 | if rand.Int()%10 == 0 { 33 | return nil, errors.New("intentional error") 34 | } 35 | 36 | resp, err := http.Get("https://api.ipify.org/") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | defer resp.Body.Close() 42 | 43 | respBytes, err := io.ReadAll(resp.Body) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return net.ParseIP(strings.TrimSpace(string(respBytes))), nil 49 | } 50 | 51 | func (u *IPUtilImpl) LocalIPs() ([]net.IP, error) { 52 | if rand.Int()%10 == 0 { 53 | return nil, errors.New("intentional error") 54 | } 55 | 56 | ifaces, err := net.Interfaces() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | var ips []net.IP 62 | for _, i := range ifaces { 63 | addrs, err := i.Addrs() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | for _, addr := range addrs { 69 | var ip net.IP 70 | switch v := addr.(type) { 71 | case *net.IPNet: 72 | ip = v.IP 73 | case *net.IPAddr: 74 | ip = v.IP 75 | } 76 | 77 | ips = append(ips, ip) 78 | } 79 | } 80 | 81 | return ips, nil 82 | } 83 | 84 | type Metrics struct { 85 | total *atomic.Int64 86 | errCnt *atomic.Int64 87 | successCnt *atomic.Int64 88 | durations []time.Duration 89 | errs []error 90 | mu *sync.Mutex 91 | } 92 | 93 | func (m *Metrics) Total(_ context.Context, name, pkg, intr, method string) { 94 | fmt.Println("Total", pkg, intr, name) 95 | m.total.Add(1) 96 | } 97 | func (m *Metrics) Failure(_ context.Context, name, pkg, intr, method string, d time.Duration, err error) { 98 | fmt.Println("Failure", pkg, intr, method) 99 | m.errCnt.Add(1) 100 | m.mu.Lock() 101 | defer m.mu.Unlock() 102 | 103 | m.errs = append(m.errs, err) 104 | m.durations = append(m.durations, d) 105 | } 106 | func (m *Metrics) Success(_ context.Context, name, pkg, intr, method string, d time.Duration) { 107 | fmt.Println("Success", pkg, intr, method) 108 | m.successCnt.Add(1) 109 | m.mu.Lock() 110 | defer m.mu.Unlock() 111 | 112 | m.durations = append(m.durations, d) 113 | } 114 | 115 | func (m *Metrics) String() string { 116 | m.mu.Lock() 117 | defer m.mu.Unlock() 118 | 119 | dAvg := m.durationAVG() 120 | errors := m.errors() 121 | f := `Summary: 122 | total: %d 123 | errCnt: %d 124 | successCnt: %d 125 | durationAvg: %s 126 | errors: %s 127 | ` 128 | return fmt.Sprintf( 129 | f, 130 | m.total.Load(), 131 | m.errCnt.Load(), 132 | m.successCnt.Load(), 133 | dAvg.String(), 134 | errors, 135 | ) 136 | 137 | } 138 | 139 | func (m *Metrics) durationAVG() time.Duration { 140 | var t time.Duration 141 | for _, d := range m.durations { 142 | t += d 143 | } 144 | 145 | return time.Duration(int(t.Nanoseconds()) / len(m.durations)) 146 | } 147 | 148 | func (m *Metrics) errors() string { 149 | errs := errors.Join(m.errs...) 150 | if errs != nil { 151 | return errs.Error() 152 | } 153 | return "" 154 | } 155 | 156 | func main() { 157 | 158 | m := &Metrics{ 159 | total: &atomic.Int64{}, 160 | errCnt: &atomic.Int64{}, 161 | successCnt: &atomic.Int64{}, 162 | durations: []time.Duration{}, 163 | errs: []error{}, 164 | mu: &sync.Mutex{}, 165 | } 166 | 167 | uPromWrapGen := NewIPUtilPrometheusWrapperImpl("iputil", &IPUtilImpl{}, m) 168 | 169 | for i := 0; i < 100; i++ { 170 | uPromWrapGen.PublicIP() 171 | uPromWrapGen.LocalIPs() 172 | time.Sleep(200 * time.Millisecond) 173 | } 174 | 175 | fmt.Println(m.String()) 176 | } 177 | -------------------------------------------------------------------------------- /examples/readme_usage/main.misura.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/itzloop/misura. DO NOT EDIT! 2 | // RANDOM_HEX=2C37461E 3 | // This is used to avoid name colision. Storing this and then 4 | // using it will help us avoid unnecessary changes that will 5 | // pollute git changes. 6 | package main 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "net" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // IPUtilPrometheusWrapperImpl wraps IPUtil and adds metrics like: 17 | // 1. success count 18 | // 2. error count 19 | // 3. total count 20 | // 4. duration 21 | type IPUtilPrometheusWrapperImpl struct { 22 | // TODO what are fields are required 23 | name string 24 | intr string 25 | wrapped IPUtil 26 | metrics interface { 27 | // Failure will be called when err != nil passing the duration and err to it 28 | Failure(ctx context.Context, name, pkg, intr, method string, duration time.Duration, err error) 29 | // Success will be called if err == nil passing the duration 30 | Success(ctx context.Context, name, pkg, intr, method string, duration time.Duration) 31 | // Total will be called as soon as the function is called. 32 | Total(ctx context.Context, name, pkg, intr, method string) 33 | } 34 | } 35 | 36 | func NewIPUtilPrometheusWrapperImpl( 37 | name string, 38 | wrapped IPUtil, 39 | metrics interface { 40 | // Failure will be called when err != nil passing the duration and err to it 41 | Failure(ctx context.Context, name, pkg, intr, method string, duration time.Duration, err error) 42 | // Success will be called if err == nil passing the duration 43 | Success(ctx context.Context, name, pkg, intr, method string, duration time.Duration) 44 | // Total will be called as soon as the function is called. 45 | Total(ctx context.Context, name, pkg, intr, method string) 46 | }, 47 | ) *IPUtilPrometheusWrapperImpl { 48 | var intr string 49 | splited := strings.Split(fmt.Sprintf("%T", wrapped), ".") 50 | if len(splited) != 2 { 51 | intr = "IPUtil" 52 | } else { 53 | intr = splited[1] 54 | } 55 | 56 | return &IPUtilPrometheusWrapperImpl{ 57 | name: name, 58 | intr: intr, 59 | wrapped: wrapped, 60 | metrics: metrics, 61 | } 62 | } 63 | 64 | // PublicIP wraps another instance of IPUtil and 65 | // adds prometheus metrics. See PublicIP on IPUtilPrometheusWrapperImpl.wrapped for 66 | // more information. 67 | func (w *IPUtilPrometheusWrapperImpl) PublicIP() (net.IP, error) { 68 | // TODO time package conflicts 69 | start2C37461E := time.Now() 70 | w.metrics.Total(context.Background(), w.name, "main", w.intr, "PublicIP") 71 | a, err := w.wrapped.PublicIP() 72 | duration2C37461E := time.Since(start2C37461E) 73 | if err != nil { 74 | w.metrics.Failure(context.Background(), w.name, "main", w.intr, "PublicIP", duration2C37461E, err) 75 | // TODO find a way to add default values here and return the error. for now return the same thing :) 76 | return a, err 77 | } 78 | w.metrics.Success(context.Background(), w.name, "main", w.intr, "PublicIP", duration2C37461E) 79 | 80 | return a, err 81 | } 82 | 83 | // LocalIPs wraps another instance of IPUtil and 84 | // adds prometheus metrics. See LocalIPs on IPUtilPrometheusWrapperImpl.wrapped for 85 | // more information. 86 | func (w *IPUtilPrometheusWrapperImpl) LocalIPs() ([]net.IP, error) { 87 | // TODO time package conflicts 88 | start2C37461E := time.Now() 89 | w.metrics.Total(context.Background(), w.name, "main", w.intr, "LocalIPs") 90 | a, err := w.wrapped.LocalIPs() 91 | duration2C37461E := time.Since(start2C37461E) 92 | if err != nil { 93 | w.metrics.Failure(context.Background(), w.name, "main", w.intr, "LocalIPs", duration2C37461E, err) 94 | // TODO find a way to add default values here and return the error. for now return the same thing :) 95 | return a, err 96 | } 97 | w.metrics.Success(context.Background(), w.name, "main", w.intr, "LocalIPs", duration2C37461E) 98 | 99 | return a, err 100 | } 101 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itzloop/misura 2 | 3 | go 1.21.4 4 | 5 | toolchain go1.21.5 6 | 7 | require golang.org/x/tools v0.16.1 8 | 9 | require ( 10 | github.com/davecgh/go-spew v1.1.1 // indirect 11 | github.com/pmezard/go-difflib v1.0.0 // indirect 12 | gopkg.in/yaml.v3 v3.0.1 // indirect 13 | ) 14 | 15 | require ( 16 | github.com/stretchr/testify v1.8.4 17 | golang.org/x/mod v0.14.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 8 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 9 | golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= 10 | golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/itzloop/misura/config" 13 | "github.com/itzloop/misura/wrapper" 14 | 15 | "embed" 16 | ) 17 | 18 | var ( 19 | version = "v0.0.9-bf7ba05" 20 | commit = "bf7ba053f15ab549f26950d8b6ba20facfbc395c" 21 | builtBy = "golang" 22 | date = "2024-06-16 23:36:08+00:00" 23 | progDesc = "misura (Italian for measure) gives insight about a golang type by generating a wrapper" 24 | website = "https://sinashabani.dev" 25 | ) 26 | 27 | //go:embed templates/*.gotmpl 28 | var f embed.FS 29 | 30 | //go:embed ascii.txt 31 | var asciiArt string 32 | 33 | func versionString() string { 34 | t := template.Must(template.ParseFS(f, "templates/version.gotmpl")) 35 | buf := bytes.Buffer{} 36 | 37 | t.Execute(&buf, map[string]string{ 38 | "ASCII": asciiArt, 39 | "ProgDesc": progDesc, 40 | "Website": website, 41 | "ProgVer": version, 42 | "GitCommit": commit, 43 | "BuildDate": date, 44 | "BuiltBy": builtBy, 45 | }) 46 | 47 | return buf.String() 48 | } 49 | 50 | func main() { 51 | cfg := config.NewConfig(os.Args[0]) 52 | 53 | // error will be handled by flag.ExitOnError 54 | cfg.Parse(os.Args[1:]) 55 | 56 | if *cfg.ShowVersion { 57 | fmt.Print(versionString()) 58 | os.Exit(0) 59 | } 60 | 61 | if len(*cfg.Measures) == 0 { 62 | *cfg.Measures = append(*cfg.Measures, "all") 63 | } 64 | 65 | if os.Getenv("GOFILE") != "" { 66 | *cfg.FilePath = os.Getenv("GOFILE") 67 | fmt.Println("using GOFILE=", *cfg.FilePath) 68 | } 69 | 70 | fmt.Printf("running command: %s\n", strings.Join(os.Args, " ")) 71 | 72 | cwd, err := os.Getwd() 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | *cfg.FilePath = path.Join(cwd, *cfg.FilePath) 78 | fmt.Printf("generating wrapper for '%s'\n", *cfg.FilePath) 79 | 80 | generator, err := wrapper.NewWrapperGenerator(wrapper.GeneratorOpts{ 81 | Metrics: []string(*cfg.Measures), 82 | FormatImports: *cfg.FormatImports, 83 | Template: templates(), 84 | }) 85 | if err != nil { 86 | log.Fatalf("failed to create WrapperGenerator: %v\n", err) 87 | } 88 | 89 | // parse comments for //misura: 90 | cv, err := wrapper.NewCommentVisitor(*cfg.FilePath) 91 | if err != nil { 92 | log.Fatalf("failed to create CommentVisitor: %v\n", err) 93 | } 94 | 95 | err = cv.Walk() 96 | if err != nil { 97 | log.Fatalf("failed to walk over ast: %v\n", err) 98 | } 99 | 100 | visitor, err := wrapper.NewTypeVisitor(generator, wrapper.TypeVisitorOpts{ 101 | FilePath: *cfg.FilePath, 102 | Targets: append([]string(*cfg.Types), cv.Targets()...), 103 | }) 104 | if err != nil { 105 | log.Fatalf("failed to create TypeVisitor: %v\n", err) 106 | } 107 | 108 | err = visitor.Walk() 109 | if err != nil { 110 | log.Fatalf("failed to walk over ast: %v\n", err) 111 | } 112 | } 113 | 114 | func templates() *template.Template { 115 | tmpl := template.New("wrapper") 116 | return template.Must(tmpl.ParseFS(f, "templates/*.gotmpl")) 117 | } 118 | -------------------------------------------------------------------------------- /pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "pre-commit.sh: Modifying main.go" 4 | 5 | WD="$(pwd)" 6 | sed -i -E \ 7 | -e "s/version([ ]*)= \"(.*)\"/version\1= \"$(git describe --tags --abbrev=0)-$(git rev-parse --short HEAD)\"/" \ 8 | -e "s/commit([ ]*)= \"(.*)\"/commit\1= \"$(git rev-parse HEAD)\"/" \ 9 | -e "s/builtBy([ ]*)= \"(.*)\"/builtBy\1= \"golang\"/" \ 10 | -e "s/date([ ]*)= \"(.*)\"/date\1= \"$(TZ=UTC date --rfc-3339=seconds)\"/" \ 11 | -e "s/website([ ]*)= \"(.*)\"/website\1= \"https:\/\/sinashabani.dev\"/" \ 12 | "$WD/main.go" 13 | 14 | git add "$WD/main.go" 15 | 16 | -------------------------------------------------------------------------------- /templates/header.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/itzloop/misura. DO NOT EDIT! 2 | // RANDOM_HEX={{ .RandomHex }} 3 | // This is used to avoid name colision. Storing this and then 4 | // using it will help us avoid unnecessary changes that will 5 | // pollute git changes. 6 | package {{ .PackageName }} 7 | 8 | {{ .Imports }} 9 | -------------------------------------------------------------------------------- /templates/interface_decl.gotmpl: -------------------------------------------------------------------------------- 1 | metrics interface{ 2 | {{- if .HasError }} 3 | // Failure will be called when err != nil passing the {{ if .HasDuration }}duration and {{ end }}err to it 4 | Failure(ctx context.Context, name, pkg, intr, method string,{{ if .HasDuration }} duration time.Duration,{{ end }} err error) 5 | {{- end }} 6 | 7 | {{- if .HasSuccess }} 8 | // Success will be called if err == nil {{ if .HasDuration }}passing the duration{{ end }} 9 | Success(ctx context.Context, name, pkg, intr, method string, {{ if .HasDuration }} duration time.Duration,{{ end }}) 10 | {{- end }} 11 | 12 | {{- if .HasTotal }} 13 | // Total will be called as soon as the function is called. 14 | Total(ctx context.Context, name, pkg, intr, method string) 15 | {{- end }} 16 | } 17 | -------------------------------------------------------------------------------- /templates/interface_decl_comma.gotmpl: -------------------------------------------------------------------------------- 1 | metrics interface{ 2 | {{- if .HasError }} 3 | // Failure will be called when err != nil passing the {{ if .HasDuration }}duration and {{ end }}err to it 4 | Failure(ctx context.Context, name, pkg, intr, method string,{{ if .HasDuration }} duration time.Duration,{{ end }} err error) 5 | {{- end }} 6 | 7 | {{- if .HasSuccess }} 8 | // Success will be called if err == nil {{ if .HasDuration }}passing the duration{{ end }} 9 | Success(ctx context.Context, name, pkg, intr, method string, {{ if .HasDuration }} duration time.Duration,{{ end }}) 10 | {{- end }} 11 | 12 | {{- if .HasTotal }} 13 | // Total will be called as soon as the function is called. 14 | Total(ctx context.Context, name, pkg, intr, method string) 15 | {{- end }} 16 | }, 17 | -------------------------------------------------------------------------------- /templates/version.gotmpl: -------------------------------------------------------------------------------- 1 | {{ .ASCII }} 2 | {{ .ProgDesc }} 3 | {{ .Website }} 4 | 5 | ProgramVersion: {{ .ProgVer }} 6 | GitCommit: {{ .GitCommit }} 7 | BuildDate: {{ .BuildDate }} 8 | BuiltBy: {{ .BuiltBy }} 9 | -------------------------------------------------------------------------------- /templates/wrapper.gotmpl: -------------------------------------------------------------------------------- 1 | {{- $wn := printf "%sPrometheusWrapperImpl" .WrapperTypeName}} 2 | {{- $duration := printf "duration%s" $.RandomHex }} 3 | {{- $start := printf "start%s" $.RandomHex }} 4 | {{- template "header.gotmpl" $}} 5 | // {{$wn}} wraps {{ .WrapperTypeName }} and adds metrics like: 6 | // 1. success count 7 | // 2. error count 8 | // 3. total count 9 | // 4. duration 10 | type {{$wn}} struct { 11 | // TODO what are fields are required 12 | name string 13 | intr string 14 | wrapped {{.WrapperTypeName}} 15 | {{ template "interface_decl.gotmpl" .}} 16 | } 17 | 18 | func New{{$wn}}( 19 | name string, 20 | wrapped {{.WrapperTypeName}}, 21 | {{ template "interface_decl_comma.gotmpl" . -}} 22 | ) *{{$wn}} { 23 | var intr string 24 | splited := strings.Split(fmt.Sprintf("%T", wrapped), ".") 25 | if len(splited) != 2 { 26 | intr = "{{ $.WrapperTypeName }}" 27 | } else { 28 | intr = splited[1] 29 | } 30 | 31 | return &{{$wn}}{ 32 | name: name, 33 | intr: intr, 34 | wrapped: wrapped, 35 | metrics: metrics, 36 | } 37 | } 38 | 39 | {{range .MethodList }} 40 | // {{ .MethodName }} wraps another instance of {{ $.WrapperTypeName }} and 41 | // adds prometheus metrics. See {{ .MethodName }} on {{$wn}}.wrapped for 42 | // more information. 43 | func (w *{{$wn}}) {{ .MethodSigFull }} { 44 | {{- if .HasError }} 45 | // TODO time package conflicts 46 | {{ $start }} := time.Now() 47 | {{- end }} 48 | 49 | {{- if and .HasCtx $.HasTotal }} 50 | w.metrics.Total({{ .Ctx }}, w.name, "{{ $.PackageName }}", w.intr, "{{ .MethodName }}") 51 | {{- else if $.HasTotal }} 52 | w.metrics.Total(context.Background(), w.name, "{{ $.PackageName }}", w.intr, "{{ .MethodName }}") 53 | {{- end}} 54 | {{- if eq .ResultNames "" }} 55 | w.wrapped.{{.MethodName}}({{ .MethodParamNames }}) 56 | {{- else if .NamedResults }} 57 | {{.ResultNames }} = w.wrapped.{{.MethodName}}({{ .MethodParamNames }}) 58 | {{- else }} 59 | {{.ResultNames }} := w.wrapped.{{.MethodName}}({{ .MethodParamNames }}) 60 | {{- end}} 61 | {{- if .HasError }} 62 | {{ $duration }} := time.Since({{$start}}) 63 | if err != nil { 64 | {{- if and .HasCtx $.HasError }} 65 | w.metrics.Failure({{ .Ctx }}, w.name, "{{ $.PackageName }}", w.intr, "{{ .MethodName }}"{{if $.HasDuration }}, {{ $duration }}{{end}}, err) 66 | {{- else if $.HasError}} 67 | w.metrics.Failure(context.Background(), w.name, "{{ $.PackageName }}", w.intr, "{{ .MethodName }}"{{if $.HasDuration }}, {{ $duration }}{{end}}, err) 68 | {{- end}} 69 | // TODO find a way to add default values here and return the error. for now return the same thing :) 70 | return {{.ResultNames }} 71 | } 72 | 73 | {{- if and .HasCtx $.HasSuccess }} 74 | // TODO if method has no error does success matter or not? 75 | w.metrics.Success({{ .Ctx }}, w.name, "{{ $.PackageName }}", w.intr, "{{ .MethodName }}"{{if $.HasDuration }}{{if $.HasDuration }}, {{ $duration }}{{end}}{{end}}) 76 | {{- else if $.HasSuccess }} 77 | w.metrics.Success(context.Background(), w.name, "{{ $.PackageName }}", w.intr, "{{ .MethodName }}"{{if $.HasDuration }}, {{ $duration }}{{end}}) 78 | {{- end}} 79 | {{- end }} 80 | 81 | return {{.ResultNames }} 82 | } 83 | {{ end }} 84 | -------------------------------------------------------------------------------- /wrapper/comment_visitor.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "os" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | type CommentVisitor struct { 13 | targets []string 14 | p string 15 | text []byte 16 | err error 17 | } 18 | 19 | func NewCommentVisitor(p string) (*CommentVisitor, error) { 20 | f, err := os.ReadFile(p) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &CommentVisitor{ 26 | p: p, 27 | text: f, 28 | }, nil 29 | } 30 | 31 | func (cv *CommentVisitor) Walk() error { 32 | fset := token.NewFileSet() 33 | root, err := parser.ParseFile(fset, path.Base(cv.p), cv.text, parser.ParseComments) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | ast.Walk(cv, root) 39 | return cv.err 40 | } 41 | 42 | func (cv *CommentVisitor) Visit(nRaw ast.Node) ast.Visitor { 43 | if nRaw == nil { 44 | return nil 45 | } 46 | 47 | switch n := nRaw.(type) { 48 | case *ast.Comment: 49 | if !strings.Contains(n.Text, "//misura:") { 50 | return cv 51 | } 52 | 53 | cv.targets = append(cv.targets, strings.Replace(n.Text, "//misura:", "", 1)) 54 | } 55 | 56 | return cv 57 | } 58 | 59 | func (cv *CommentVisitor) Targets() []string { 60 | return cv.targets 61 | } 62 | -------------------------------------------------------------------------------- /wrapper/comment_visitor_test.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCommentVisitor(t *testing.T) { 12 | expectedTargets := []string{ 13 | "MagicNamedParamsAndResults", 14 | "MagicUnnamedAndNamedParamsAndResults", 15 | "MagicUnderscoreNames", 16 | "MagicNoParams", 17 | "MagicNoResult", 18 | } 19 | 20 | 21 | wd := copyFilesHelper(t) 22 | cv, err := NewCommentVisitor(path.Join(wd, "magic_comment.go")) 23 | require.NoError(t,err) 24 | 25 | err = cv.Walk() 26 | require.NoError(t, err) 27 | 28 | assert.ElementsMatch(t, expectedTargets, cv.Targets()) 29 | } 30 | -------------------------------------------------------------------------------- /wrapper/generator.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path" 11 | "strings" 12 | "text/template" 13 | 14 | "github.com/itzloop/misura/wrapper/types" 15 | "golang.org/x/tools/imports" 16 | ) 17 | 18 | // TODO how can we go about using multiple template files 19 | type GeneratorOpts struct { 20 | // FormatImports, if set to true, will be used 21 | // call imports.Process 22 | FormatImports bool 23 | 24 | // Template will be used to generate the wrapper 25 | // If this is null TemplateStr or TemplatePath 26 | // will be used. 27 | Template *template.Template 28 | 29 | // TemplateStr will be used to generate the wrapper 30 | // If this is null Template or TemplatePath 31 | // will be used. 32 | TemplateStr string 33 | 34 | // TemplatePath will be used to generate the wrapper 35 | // The code will first read the contents. If this is 36 | // null Template or TemplateStr will be used. 37 | // TODO can this be a directory? 38 | TemplatePath string 39 | 40 | // Suffix will be used to name the generated wrapper. 41 | // GOFILE.Suffix.go. By default misura will be used.j 42 | Suffix string 43 | 44 | // Metrics will be used to decided what metrics to include. 45 | // Possible values are: 46 | // 1. duration 47 | // 2. total 48 | // 3. error 49 | // 4. success 50 | // 5. all 51 | // If all is specified then others will be ignored. 52 | Metrics types.Strings 53 | } 54 | 55 | type TemplateVals struct { 56 | PackageName string 57 | WrapperTypeName string 58 | MethodList []types.Method 59 | Imports string 60 | StartTimeName string 61 | DurationName string 62 | RandomHex string 63 | 64 | // metrics 65 | HasDuration bool 66 | HasTotal bool 67 | HasError bool 68 | HasSuccess bool 69 | } 70 | 71 | type WrapperGenerator struct { 72 | tmpl *template.Template 73 | opts GeneratorOpts 74 | } 75 | 76 | func MustNewWrapperGenerator(w *WrapperGenerator, err error) *WrapperGenerator { 77 | if err != nil { 78 | log.Fatalln("failed to create WrapperGenerator:", err) 79 | } 80 | 81 | return w 82 | } 83 | 84 | func NewWrapperGenerator(opts GeneratorOpts) (*WrapperGenerator, error) { 85 | var ( 86 | w = WrapperGenerator{opts: opts} 87 | err error 88 | ) 89 | 90 | if w.opts.Template != nil { 91 | w.tmpl = w.opts.Template 92 | } else if strings.TrimSpace(w.opts.TemplateStr) != "" { 93 | w.tmpl, err = template.New("wrapper.gotmpl").Parse(string(w.opts.TemplateStr)) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | } else if strings.TrimSpace(w.opts.TemplatePath) != "" { 99 | w.tmpl, err = template.New("wrapper.gotmpl").ParseFiles(w.opts.TemplatePath) 100 | if err != nil { 101 | return nil, err 102 | } 103 | } else { 104 | return nil, errors.New("no template provided") 105 | } 106 | 107 | if strings.TrimSpace(w.opts.Suffix) == "" { 108 | w.opts.Suffix = "misura" 109 | } 110 | 111 | return &w, nil 112 | } 113 | 114 | func (w *WrapperGenerator) Generate(outPath, filename string, tmplVals TemplateVals) error { 115 | var ( 116 | b = &bytes.Buffer{} 117 | processed []byte 118 | filenameSuffixed = fmt.Sprintf("%s.%s.go", strings.Replace(filename, ".go", "", 1), w.opts.Suffix) 119 | p = path.Join(outPath, filenameSuffixed) 120 | err error 121 | ) 122 | 123 | fmt.Println(w.opts.Metrics) 124 | if len(w.opts.Metrics) == 0 || w.opts.Metrics.Exists("all") { 125 | tmplVals.HasDuration = true 126 | tmplVals.HasTotal = true 127 | tmplVals.HasError = true 128 | tmplVals.HasSuccess = true 129 | } else { 130 | if w.opts.Metrics.Exists("duration") { 131 | tmplVals.HasDuration = true 132 | } 133 | 134 | if w.opts.Metrics.Exists("total") { 135 | tmplVals.HasTotal = true 136 | } 137 | 138 | if w.opts.Metrics.Exists("error") { 139 | tmplVals.HasError = true 140 | } 141 | 142 | if w.opts.Metrics.Exists("success") { 143 | tmplVals.HasSuccess = true 144 | } 145 | } 146 | 147 | tmplVals.RandomHex, err = getRandomHex(p, tmplVals.RandomHex) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | if err = w.tmpl.ExecuteTemplate(b, "wrapper.gotmpl", tmplVals); err != nil { 153 | return err 154 | } 155 | 156 | processed, err = formatImports(p, b, w.opts.FormatImports) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | f, err := os.Create(p) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | fmt.Printf("writing to %s\n", f.Name()) 167 | _, err = f.Write(processed) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | return nil 173 | } 174 | 175 | func formatImports(filename string, b *bytes.Buffer, format bool) ([]byte, error) { 176 | if format { 177 | processed, err := imports.Process(filename, b.Bytes(), nil) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | return processed, nil 183 | } 184 | 185 | return b.Bytes(), nil 186 | 187 | } 188 | 189 | func getRandomHex(p, randomHex string) (string, error) { 190 | f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 0644) 191 | if err != nil { 192 | return "", err 193 | } 194 | defer f.Close() 195 | 196 | sc := bufio.NewScanner(f) 197 | 198 | if sc.Scan() && sc.Scan() { 199 | newRandomHex := strings.Replace(sc.Text(), "// RANDOM_HEX=", "", 1) 200 | if strings.TrimSpace(newRandomHex) != "" { 201 | fmt.Printf("reusing random hex: %s\n", newRandomHex) 202 | return newRandomHex, nil 203 | } 204 | } 205 | 206 | return randomHex, nil 207 | } 208 | -------------------------------------------------------------------------------- /wrapper/helpers.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | func genNameHelper(count int) func() string { 4 | start := -1 5 | if count < 1 { 6 | count = 1 7 | } 8 | 9 | alphabet := "abcdefghijklmnopqrstuvwxyz" 10 | 11 | return func() string { 12 | start++ 13 | if start == len(alphabet) { 14 | start = 0 15 | count++ 16 | } 17 | 18 | if start+count <= len(alphabet) { 19 | return string([]byte(alphabet)[start : start+count]) 20 | } 21 | 22 | return string([]byte(alphabet)[start:start+count]) + string([]byte(alphabet)[0:start+count-len(alphabet)]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /wrapper/test_samples/magic_comment.go: -------------------------------------------------------------------------------- 1 | package testsamples 2 | 3 | //misura:MagicNamedParamsAndResults 4 | type MagicNamedParamsAndResults interface { 5 | Method1(a string, b *int, c []byte) (s string, err error) 6 | } 7 | 8 | //misura:MagicUnnamedAndNamedParamsAndResults 9 | type MagicUnnamedAndNamedParamsAndResults interface { 10 | Method1(a string, b *int, c []byte) (s string, err error) 11 | Method2(a string, b *int, c []byte) (string, error) 12 | Method3(string, *int, []byte) (string, error) 13 | Method4(string, *int, []byte) (s string, err error) 14 | } 15 | 16 | //misura:MagicUnderscoreNames 17 | type MagicUnderscoreNames interface { 18 | Method1(_ string, b *int, c []byte) (s string, err error) 19 | Method2(a string, _ *int, c []byte) (s string, err error) 20 | Method3(a string, b *int, _ []byte) (s string, err error) 21 | Method4(a string, b *int, c []byte) (_ string, err error) 22 | Method5(a string, b *int, c []byte) (s string, _ error) 23 | Method6(_ string, b *int, _ []byte) (s string, _ error) 24 | Method7(_ string, _ *int, _ []byte) (s string, _ error) 25 | Method8(_ string, _ *int, _ []byte) (_ string, _ error) 26 | } 27 | 28 | //misura:MagicNoParams 29 | type MagicNoParams interface { 30 | Method1() error 31 | Method2() (s string, err error) 32 | Method3() (string, error) 33 | } 34 | 35 | //misura:MagicNoResult 36 | type MagicNoResult interface { 37 | Method1(s string) 38 | Method2(n int) 39 | Method3(a, b, c int, s string) 40 | Method4(a, _, c int, _ string) 41 | } 42 | 43 | -------------------------------------------------------------------------------- /wrapper/test_samples/mytime/mytime.go: -------------------------------------------------------------------------------- 1 | package mytime 2 | 3 | import "time" 4 | 5 | type Time time.Time 6 | -------------------------------------------------------------------------------- /wrapper/test_samples/test.go: -------------------------------------------------------------------------------- 1 | package testsamples 2 | 3 | 4 | type NamedParamsAndResults interface { 5 | Method1(a string, b *int, c []byte) (s string, err error) 6 | } 7 | 8 | 9 | type UnnamedAndNamedParamsAndResults interface { 10 | Method1(a string, b *int, c []byte) (s string, err error) 11 | Method2(a string, b *int, c []byte) (string, error) 12 | Method3(string, *int, []byte) (string, error) 13 | Method4(string, *int, []byte) (s string, err error) 14 | } 15 | 16 | type UnderscoreNames interface { 17 | Method1(_ string, b *int, c []byte) (s string, err error) 18 | Method2(a string, _ *int, c []byte) (s string, err error) 19 | Method3(a string, b *int, _ []byte) (s string, err error) 20 | Method4(a string, b *int, c []byte) (_ string, err error) 21 | Method5(a string, b *int, c []byte) (s string, _ error) 22 | Method6(_ string, b *int, _ []byte) (s string, _ error) 23 | Method7(_ string, _ *int, _ []byte) (s string, _ error) 24 | Method8(_ string, _ *int, _ []byte) (_ string, _ error) 25 | } 26 | 27 | 28 | type NoParams interface { 29 | Method1() error 30 | Method2() (s string, err error) 31 | Method3() (string, error) 32 | } 33 | 34 | type NoResult interface { 35 | Method1(s string) 36 | Method2(n int) 37 | Method3(a, b, c int, s string) 38 | Method4(a, _, c int, _ string) 39 | } 40 | 41 | // TODO 42 | // {filename: "test.go", target: "ConflictDuration"}, 43 | // {filename: "test.go", target: "ConflictTimePackage"}, 44 | -------------------------------------------------------------------------------- /wrapper/test_samples/time.go: -------------------------------------------------------------------------------- 1 | package testsamples 2 | 3 | import ( 4 | time "github.com/itzloop/misura/wrapper/test_samples/mytime" 5 | time2 "time" 6 | ) 7 | 8 | type TimeConflict interface { 9 | Method1(t1 time.Time, t2 time.Time) time2.Time 10 | } 11 | -------------------------------------------------------------------------------- /wrapper/types/func_param.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type FuncParam struct { 9 | Name string 10 | Type string 11 | } 12 | 13 | type FuncParams []FuncParam 14 | 15 | func (f FuncParams) JoinNames() string { 16 | str := "" 17 | for i, p := range f { 18 | n := p.Name 19 | if strings.Contains(p.Type, "...") { 20 | n += "..." 21 | } 22 | if i == len(f)-1 { 23 | str += n 24 | continue 25 | } 26 | str += n + ", " 27 | } 28 | 29 | return str 30 | } 31 | 32 | func (f FuncParams) JoinTypes() string { 33 | str := "" 34 | for i, p := range f { 35 | if i == len(f)-1 { 36 | str += fmt.Sprintf("%s", p.Type) 37 | continue 38 | } 39 | str += fmt.Sprintf("%s, ", p.Type) 40 | } 41 | 42 | return str 43 | } 44 | 45 | func (f FuncParams) Join() string { 46 | str := "" 47 | for i, p := range f { 48 | if i == len(f)-1 { 49 | str += fmt.Sprintf("%s %s", p.Name, p.Type) 50 | continue 51 | } 52 | str += fmt.Sprintf("%s %s, ", p.Name, p.Type) 53 | } 54 | 55 | return str 56 | } 57 | -------------------------------------------------------------------------------- /wrapper/types/method.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Method struct { 4 | MethodSigFull string 5 | MethodName string 6 | MethodParamNames string 7 | ResultNames string 8 | NamedResults bool 9 | HasError bool 10 | HasCtx bool 11 | Ctx string 12 | } 13 | -------------------------------------------------------------------------------- /wrapper/types/target.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Strings []string 4 | 5 | func (ts Strings) Exists(target string) bool { 6 | for _, t := range ts { 7 | if t == target { 8 | return true 9 | } 10 | } 11 | 12 | return false 13 | } 14 | -------------------------------------------------------------------------------- /wrapper/visitor.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "go/ast" 9 | "go/parser" 10 | "go/token" 11 | "os" 12 | "path" 13 | "strings" 14 | 15 | "github.com/itzloop/misura/wrapper/types" 16 | ) 17 | 18 | type TypeVisitorOpts struct { 19 | FilePath string 20 | Targets types.Strings 21 | } 22 | 23 | type TypeVisitor struct { 24 | err error 25 | 26 | opts TypeVisitorOpts 27 | 28 | fset *token.FileSet 29 | packageName string 30 | imports string 31 | 32 | text []byte 33 | 34 | // TODO make this interface 35 | g *WrapperGenerator 36 | } 37 | 38 | func NewTypeVisitor(g *WrapperGenerator, opts TypeVisitorOpts) (*TypeVisitor, error) { 39 | f, err := os.ReadFile(opts.FilePath) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return &TypeVisitor{ 44 | g: g, 45 | text: f, 46 | opts: opts, 47 | }, nil 48 | } 49 | 50 | func (t *TypeVisitor) Walk() error { 51 | fset := token.NewFileSet() 52 | root, err := parser.ParseFile(fset, path.Base(t.opts.FilePath), t.text, parser.ParseComments) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | ast.Walk(t, root) 58 | return t.err 59 | } 60 | 61 | func (t *TypeVisitor) Visit(nRaw ast.Node) ast.Visitor { 62 | if nRaw == nil { 63 | return nil 64 | } 65 | 66 | switch n := nRaw.(type) { 67 | case *ast.File: 68 | t.packageName = n.Name.String() 69 | case *ast.GenDecl: 70 | // we only care about import statements 71 | if n.Tok.String() != "import" { 72 | return t 73 | } 74 | 75 | // copy all the imports from the file to the wrapper 76 | t.imports = string(t.text[n.Pos()-1 : n.End()-1]) 77 | case *ast.TypeSpec: 78 | switch x := n.Type.(type) { 79 | case *ast.InterfaceType: 80 | // TODO support unnamed interfaces? 81 | // Ignore unnamed interfaces for now 82 | if n.Name.String() == "" { 83 | fmt.Printf("ignoring unnamed interface\n") 84 | return nil 85 | } 86 | if !t.opts.Targets.Exists(n.Name.String()) { 87 | fmt.Printf("ignoring %s since it is not a target\n", n.Name.String()) 88 | return nil 89 | } 90 | 91 | err := t.handleInterface(n.Name.String(), x) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | // we are done with this interface do not proceed further. 97 | return nil 98 | case *ast.StructType: 99 | // TODO support unnamed structs? 100 | // Ignore unnamed structs for now 101 | if n.Name.String() == "" { 102 | fmt.Printf("ignoring unnamed struct\n") 103 | return nil 104 | } 105 | 106 | if !t.opts.Targets.Exists(n.Name.String()) { 107 | fmt.Printf("ignoring %s since it is not a target\n", n.Name.String()) 108 | return nil 109 | } 110 | 111 | fmt.Println("struct type not implemented") 112 | return nil 113 | } 114 | } 115 | 116 | return t 117 | } 118 | 119 | func (t *TypeVisitor) handleInterface(intrName string, intr *ast.InterfaceType) error { 120 | if t.packageName == "" { 121 | return errors.New("TypeVisitor: package name can't be empty") 122 | } 123 | 124 | methods := make([]types.Method, 0, cap(intr.Methods.List)) 125 | for _, m := range intr.Methods.List { 126 | method := types.Method{ 127 | MethodSigFull: string(t.text[m.Pos()-1 : m.End()-1]), 128 | MethodName: m.Names[0].String(), 129 | } 130 | ft, ok := m.Type.(*ast.FuncType) 131 | if !ok { 132 | return errors.New("TODO: don't want to think about this now :)") 133 | } 134 | 135 | f := genNameHelper(1) 136 | t.handleParams(&method, ft.Params, f) 137 | t.handleResults(&method, ft.Results, f) 138 | 139 | // finally add current method to the methods slice, to use them when 140 | // populating templatess. 141 | methods = append(methods, method) 142 | 143 | } 144 | 145 | // populate template 146 | randBytes := make([]byte, 4) 147 | _, err := rand.Read(randBytes) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | // if we have mulitple targets in the same file, 153 | // split them in seperate files by including the 154 | // target in the generated file name 155 | filename := path.Base(t.opts.FilePath) 156 | if len(t.opts.Targets) > 1 { 157 | filename = filename + "." + intrName 158 | } 159 | 160 | return t.g.Generate(path.Dir(t.opts.FilePath), filename, TemplateVals{ 161 | PackageName: t.packageName, 162 | WrapperTypeName: intrName, 163 | MethodList: methods, 164 | Imports: t.imports, 165 | RandomHex: strings.ToUpper(hex.EncodeToString(randBytes)), 166 | }) 167 | } 168 | 169 | func (t *TypeVisitor) handleParams(m *types.Method, params *ast.FieldList, f func() string) types.FuncParams { 170 | var paramNames types.FuncParams 171 | 172 | if params == nil { 173 | return paramNames 174 | } 175 | 176 | // handle parameters 177 | for _, param := range params.List { 178 | // get the param type 179 | t := string(t.text[param.Type.Pos()-1 : param.Type.End()-1]) 180 | // TODO This works for now but make sure it's context.Context not any random Context 181 | // if params are unnamed(i.e. func t(int, string, bool)), 182 | // generate name by calling f 183 | if param.Names == nil { 184 | n := f() 185 | if !m.HasCtx && strings.Contains(t, "Context") { 186 | n = "ctx" 187 | m.HasCtx = true 188 | m.Ctx = n 189 | } 190 | paramNames = append(paramNames, types.FuncParam{ 191 | Name: n, 192 | Type: t, 193 | }) 194 | 195 | continue 196 | } 197 | 198 | // if params are named, iterate over names and handle them 199 | // if we encouter an underscore(_), generate a name by calling f. 200 | for _, name := range param.Names { 201 | if name.String() == "_" { 202 | n := f() 203 | if !m.HasCtx && strings.Contains(t, "Context") { 204 | n = "ctx" 205 | m.HasCtx = true 206 | m.Ctx = n 207 | } 208 | paramNames = append(paramNames, types.FuncParam{ 209 | Name: n, 210 | Type: t, 211 | }) 212 | 213 | continue 214 | } 215 | 216 | paramNames = append(paramNames, types.FuncParam{ 217 | Name: name.String(), 218 | Type: t, 219 | }) 220 | 221 | if !m.HasCtx && strings.Contains(t, "Context") { 222 | m.HasCtx = true 223 | m.Ctx = name.String() 224 | } 225 | } 226 | } 227 | 228 | // TODO This is the fist and simplest solution and probably need refactoring. 229 | // This is for handling a case where we have underscore(_) in params. since 230 | // we are calling another function we need to pass all parameters so we can't 231 | // have a parameter with underscore. we will replace everything we had as parameters 232 | // with the parameters we created either by generating new name or using old ones. 233 | m.MethodSigFull = strings.Replace( 234 | m.MethodSigFull, 235 | string(t.text[params.Pos():params.End()-2]), 236 | paramNames.Join(), 237 | 1, 238 | ) 239 | 240 | // This is used when calling the fucntion to make template simple. 241 | // wrapped.F({{ .MethodParamNames }}) => i.e. wrapped.F(a, b, c, d) 242 | m.MethodParamNames = paramNames.JoinNames() 243 | 244 | return paramNames 245 | } 246 | 247 | func (t *TypeVisitor) handleResults(m *types.Method, results *ast.FieldList, f func() string) types.FuncParams { 248 | var resultNames types.FuncParams 249 | 250 | if results == nil { 251 | return resultNames 252 | } 253 | 254 | for _, result := range results.List { 255 | t := string(t.text[result.Type.Pos()-1 : result.Type.End()-1]) 256 | // TODO multipe error? 257 | // Assume we only return one error for now and name it err. Also 258 | // set HasError to true for template to add error handling. 259 | if t == "error" { 260 | m.HasError = true 261 | resultNames = append(resultNames, types.FuncParam{ 262 | Name: "err", 263 | Type: t, 264 | }) 265 | continue 266 | } 267 | 268 | // if we have unnamedd results (i.e. f(...) (int, string, error)), 269 | // generate a name by calling f(). This is then used in getting the 270 | // return value from calling wrapped function. a, b, err = wrapped.F(...) 271 | if result.Names == nil { 272 | resultNames = append(resultNames, types.FuncParam{ 273 | Name: f(), 274 | Type: t, 275 | }) 276 | continue 277 | } 278 | 279 | // If we reach here it means we have named results so set NamedResult. 280 | // Doing that will let us use = instead of := since we have no new variable 281 | // in the right part of the expression. 282 | m.NamedResults = true 283 | 284 | // if results are named, iterate over names and handle them 285 | // if we encouter an underscore(_), generate a name by calling f. 286 | for _, name := range result.Names { 287 | if name.String() == "_" { 288 | resultNames = append(resultNames, types.FuncParam{ 289 | Name: f(), 290 | Type: t, 291 | }) 292 | continue 293 | } 294 | resultNames = append(resultNames, types.FuncParam{ 295 | Name: name.String(), 296 | Type: t, 297 | }) 298 | } 299 | } 300 | 301 | // this will replace old return values with normalized ones. 302 | // if we have named results, replace with (name type, ...) 303 | // if we have unnamed results (i.e. (string, error, ...)) 304 | // for other cases such as signle result without parantheses 305 | // replace sth :). TODO else might be redundant but i'm to 306 | // tierd to think about it now. 307 | var ( 308 | nStr string 309 | oStr string 310 | ) 311 | if m.NamedResults { 312 | nStr = resultNames.Join() 313 | oStr = string(t.text[results.Pos() : results.End()-2]) 314 | } else if results.Closing.IsValid() { 315 | nStr = resultNames.JoinTypes() 316 | oStr = string(t.text[results.Pos() : results.End()-2]) 317 | } else { 318 | nStr = resultNames.JoinTypes() 319 | oStr = string(t.text[results.Pos()-1 : results.End()-1]) 320 | } 321 | 322 | m.MethodSigFull = strings.Replace( 323 | m.MethodSigFull, 324 | oStr, 325 | nStr, 326 | 1, 327 | ) 328 | 329 | // This is used when getting results from the wrapped fucntion 330 | // to make template simple. 331 | // {{ .ResultNames }} = wrapped.F(...) => i.e. a, b, c, d := wrapped.F(...) 332 | // or 333 | // {{ .ResultNames }} := wrapped.F(...) => i.e. a, b, c, d = wrapped.F(...) 334 | m.ResultNames = resultNames.JoinNames() 335 | 336 | return resultNames 337 | } 338 | -------------------------------------------------------------------------------- /wrapper/wrapper_test.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | "text/template" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestWrapper(t *testing.T) { 18 | targets := []struct { 19 | filename string 20 | target string 21 | }{ 22 | {filename: "test.go", target: "NamedParamsAndResults"}, 23 | {filename: "test.go", target: "UnnamedAndNamedParamsAndResults"}, 24 | {filename: "test.go", target: "UnderscoreNames"}, 25 | {filename: "test.go", target: "NoParams"}, 26 | {filename: "test.go", target: "NoResult"}, 27 | // {filename: "test.go", target: "ConflictDuration"}, 28 | // {filename: "test.go", target: "ConflictTimePackage"}, 29 | } 30 | 31 | for _, target := range targets { 32 | t.Run( 33 | fmt.Sprintf("test_generated_target_%s_compliation", target.target), 34 | func(t *testing.T) { 35 | wd := copyFilesHelper(t) 36 | tv := createTypeVisitor(t, wd, target.filename, []string{target.target}) 37 | err := tv.Walk() 38 | require.NoError(t, err) 39 | require.FileExists(t, path.Join(wd, strings.ReplaceAll(target.filename, path.Ext(target.filename), ".misura.go"))) 40 | }, 41 | ) 42 | } 43 | 44 | t.Run("all_targets_compliation", func(t *testing.T) { 45 | files := map[string][]string{} 46 | for _, t := range targets { 47 | _, ok := files[t.filename] 48 | if !ok { 49 | files[t.filename] = []string{} 50 | } 51 | 52 | files[t.filename] = append(files[t.filename], t.target) 53 | } 54 | 55 | for f, target := range files { 56 | wd := copyFilesHelper(t) 57 | tv := createTypeVisitor(t, wd, f, target) 58 | err := tv.Walk() 59 | require.NoError(t, err) 60 | for _, target := range target { 61 | require.FileExists(t, path.Join(wd, strings.ReplaceAll(f, path.Ext(f), "."+target+".misura.go"))) 62 | } 63 | } 64 | 65 | }) 66 | 67 | } 68 | 69 | func createTypeVisitor(t *testing.T, cwd, filename string, targets []string) *TypeVisitor { 70 | t.Helper() 71 | 72 | tv, err := NewTypeVisitor(createGenerator(t), TypeVisitorOpts{ 73 | FilePath: path.Join(cwd, filename), 74 | Targets: targets, 75 | }) 76 | 77 | require.NoError(t, err) 78 | require.NotNil(t, tv) 79 | 80 | return tv 81 | } 82 | 83 | func createGenerator(t *testing.T) *WrapperGenerator { 84 | t.Helper() 85 | tmpl, err := template.ParseGlob(path.Join("..", "templates", "*.gotmpl")) 86 | require.NoError(t, err) 87 | require.NotNil(t, tmpl) 88 | 89 | g, err := NewWrapperGenerator(GeneratorOpts{ 90 | FormatImports: true, 91 | Template: tmpl, 92 | Metrics: []string{"all"}, 93 | }) 94 | 95 | require.NoError(t, err) 96 | require.NotNil(t, g) 97 | 98 | return g 99 | 100 | } 101 | 102 | func copyFilesHelper(t *testing.T) string { 103 | t.Helper() 104 | 105 | wd, err := os.Getwd() 106 | require.NoError(t, err) 107 | 108 | src := path.Join(wd, "test_samples") 109 | dst := t.TempDir() 110 | err = copyDir(src, dst) 111 | require.NoError(t, err) 112 | 113 | return dst 114 | 115 | } 116 | 117 | func copyDir(src, dst string) (err error) { 118 | if err = isDir(src); err != nil { 119 | return err 120 | } 121 | 122 | if err = isDir(dst); err != nil { 123 | return err 124 | } 125 | 126 | fmt.Printf("copyDir: %s -> %s\n", src, dst) 127 | 128 | return filepath.Walk(src, func(p string, info fs.FileInfo, err error) error { 129 | fmt.Println(p, info.Name(), err, path.Base(src)) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | if info.IsDir() && info.Name() != path.Base(src) { 135 | dstDir := path.Join(dst, strings.TrimPrefix(p, src)) 136 | return os.MkdirAll(dstDir, info.Mode()) 137 | } 138 | 139 | if !info.Mode().IsRegular() { 140 | return nil 141 | } 142 | 143 | to := path.Join(dst, strings.TrimPrefix(p, src)) 144 | 145 | f, err := os.Open(p) 146 | if err != nil { 147 | return nil 148 | } 149 | defer f.Close() 150 | 151 | tof, err := os.Create(to) 152 | if err != nil { 153 | return err 154 | } 155 | defer tof.Close() 156 | 157 | if err = tof.Chmod(info.Mode()); err != nil { 158 | return err 159 | } 160 | 161 | _, err = io.Copy(tof, f) 162 | return err 163 | }) 164 | } 165 | 166 | func isDir(p string) error { 167 | info, err := os.Stat(p) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | if !info.IsDir() { 173 | return fmt.Errorf("'%s' is not a directory", p) 174 | } 175 | 176 | return nil 177 | } 178 | --------------------------------------------------------------------------------