├── .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 | 
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 |
--------------------------------------------------------------------------------