├── .codecov.yml ├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bench_method.go ├── bench_state.go ├── bench_state_test.go ├── benchmark.go ├── benchmark_stream.go ├── benchmark_stream_test.go ├── benchmark_test.go ├── benchmark_unary.go ├── benchmark_unary_test.go ├── doc.go ├── encoding ├── encoding.go ├── encoding_test.go ├── encodingerror │ ├── not_found.go │ └── not_found_test.go ├── inputdecoder │ ├── decoder.go │ └── decoder_test.go ├── meta.go ├── meta_test.go ├── protobuf.go ├── protobuf_health.go ├── protobuf_health_test.go ├── protobuf_test.go ├── thrift_request.go ├── thrift_request_test.go └── yaml_to_thrift_test.go ├── etc └── bash_completion.d │ └── yab ├── go.mod ├── go.sum ├── grpc_server_util_test.go ├── handler.go ├── handler_test.go ├── integration_test.go ├── internal ├── thrifttest │ └── thrifttest.go └── yamlalias │ ├── wrap.go │ └── wrap_test.go ├── limiter ├── limiter.go └── limiter_test.go ├── logger.go ├── logger_test.go ├── main.go ├── main_test.go ├── man ├── yab.1 └── yab.html ├── options.go ├── options_test.go ├── output.go ├── parser.go ├── peerprovider ├── file.go ├── file_test.go ├── http.go ├── http_test.go ├── interface.go ├── interface_test.go ├── parse.go └── util_for_test.go ├── plugin ├── plugin.go └── plugin_test.go ├── protobuf ├── filedescriptorsets.go ├── filedescriptorsets_test.go ├── source.go ├── source_reflection.go └── source_reflection_test.go ├── raffle.bin ├── ratelimit └── time_period.go ├── request.go ├── request_test.go ├── scripts ├── cross_build.sh ├── extract_changelog.go ├── release.sh ├── updateLicense.py └── updateLicenses.sh ├── server_util_test.go ├── sorted ├── mapkeys.go └── mapkeys_test.go ├── statsd ├── dual.go ├── dual_test.go ├── prefix.go ├── prefix_test.go ├── statsd.go ├── statsd_test.go └── statsdtest │ ├── server.go │ └── server_test.go ├── statsd_fake_test.go ├── template.go ├── template_test.go ├── templateargs ├── interpolate │ ├── gen.go │ ├── generated.sh │ ├── parse.go │ ├── parse.rl │ ├── parse_test.go │ ├── types.go │ └── types_test.go ├── process.go └── process_test.go ├── test.bin ├── testdata ├── empty.txt ├── gen-go │ └── integration │ │ ├── constants.go │ │ ├── foo.go │ │ ├── tchan-integration.go │ │ └── ttypes.go ├── grpc_health.yab ├── ini │ ├── invalid │ │ └── yab │ │ │ └── defaults.ini │ └── valid │ │ └── yab │ │ └── defaults.ini ├── integration.thrift ├── invalid.json ├── invalid_peerlist.json ├── invalid_url_peerlist.txt ├── protobuf │ ├── any │ │ ├── any.pb.go │ │ ├── any.proto │ │ └── any.proto.bin │ ├── dependencies │ │ ├── combined.bin │ │ ├── dep.proto │ │ ├── dep.proto.bin │ │ ├── main.proto │ │ ├── main.proto.bin │ │ └── other.bin │ ├── generate.sh │ ├── multiroot │ │ ├── other │ │ │ └── dep.proto │ │ └── root │ │ │ ├── combined.bin │ │ │ └── main.proto │ ├── nested │ │ ├── combined.bin │ │ ├── main.proto │ │ └── other │ │ │ └── dep.proto │ └── simple │ │ ├── simple.pb.go │ │ ├── simple.proto │ │ └── simple.proto.bin ├── simple.thrift ├── templates │ ├── abspeerlist.yaml │ ├── args.yaml │ ├── bad-arg.yaml │ ├── foo-method.yaml │ ├── foo.thrift │ ├── foo.yab │ ├── invalid-fields.yab │ ├── invalid.yaml │ ├── peer.yaml │ ├── peerlist.yaml │ ├── peers.json │ ├── peers.yaml │ └── stream.yab ├── valid.json ├── valid_peerlist.json ├── valid_peerlist.txt ├── valid_peerlist.yaml ├── valid_url_peerlist.txt ├── valid_url_peerlist.yaml ├── yamltothrift │ ├── binary_data.file │ ├── binary_data.json │ ├── binary_data.out │ ├── binary_data.thrift │ ├── binary_data.yaml │ ├── complex_map.json │ ├── complex_map.out │ ├── complex_map.thrift │ ├── complex_map.yaml │ ├── int_formats.out │ ├── int_formats.thrift │ ├── int_formats.yaml │ ├── int_map.json │ ├── int_map.out │ ├── int_map.thrift │ └── int_map.yaml └── yarpc │ └── integration │ ├── foo_bar.go │ ├── fooclient │ └── client.go │ ├── fooserver │ └── server.go │ ├── footest │ └── client.go │ ├── service │ └── foo │ │ └── bar.go │ ├── types.go │ └── versioncheck.go ├── thrift ├── const.go ├── const_test.go ├── errors.go ├── fields.go ├── fields_test.go ├── from_wire.go ├── from_wire_test.go ├── options.go ├── parse.go ├── parse_test.go ├── response.go ├── response_test.go ├── to_wire.go ├── types.go └── types_test.go ├── transport.go ├── transport ├── grpc.go ├── grpc_test.go ├── http.go ├── http_test.go ├── interface.go ├── request_interceptor.go ├── request_interceptor_test.go ├── tchannel.go └── tchannel_test.go ├── transport_test.go ├── unmarshal ├── unmarshal.go └── unmarshal_test.go ├── utils_for_test.go └── version.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 80..100 3 | round: down 4 | precision: 2 5 | 6 | status: 7 | project: # measuring the overall project coverage 8 | default: # context, you can create multiple ones with custom titles 9 | enabled: yes # must be yes|true to enable this status 10 | target: auto # specify the target coverage for each commit status 11 | # option: "auto" (must increase from parent commit or pull request base) 12 | # option: "X%" a static target percentage to hit 13 | if_not_found: success # if parent is not found report status as success, error, or failure 14 | if_ci_failed: error # if ci fails report status as success, error, or failure 15 | 16 | ignore: 17 | - /testdata/**/* 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor 2 | 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bin binary 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: ['*'] 6 | tags: ['v*'] 7 | pull_request: 8 | branches: ['*'] 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Setup Go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.18.x 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Load cached dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | 32 | 33 | - name: Install 34 | run: make install 35 | 36 | - name: Test 37 | run: make test_ci 38 | 39 | - name: Upload coverage to codecov.io 40 | uses: codecov/codecov-action@v1 41 | 42 | release: 43 | runs-on: ubuntu-latest 44 | 45 | # Only release pushes of tags starting with v and if the build succeeded. 46 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 47 | needs: [build] 48 | 49 | steps: 50 | 51 | - name: Setup Go 52 | uses: actions/setup-go@v1 53 | with: 54 | go-version: 1.18.x 55 | 56 | - name: Checkout code 57 | uses: actions/checkout@v2 58 | 59 | - name: Load cached dependencies 60 | uses: actions/cache@v1 61 | with: 62 | path: ~/go/pkg/mod 63 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 64 | restore-keys: | 65 | ${{ runner.os }}-go- 66 | 67 | - name: Install 68 | run: make install 69 | 70 | - name: Release 71 | env: 72 | GITHUB_REPO: ${{ github.repository }} 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: ./scripts/release.sh ${{ github.ref }} 75 | 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | /yab 3 | *.test 4 | 5 | # Profiling output 6 | *.prof 7 | 8 | /vendor 9 | /build 10 | 11 | /cover 12 | /cover.out 13 | /.idea 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.7-alpine 2 | 3 | COPY . /go/src/github.com/yarpc/yab 4 | 5 | RUN apk update 6 | RUN apk add git 7 | 8 | RUN cd /go/src/github.com/yarpc/yab && go get github.com/Masterminds/glide 9 | RUN cd /go/src/github.com/yarpc/yab && glide install 10 | RUN cd /go/src/github.com/yarpc/yab && go install . 11 | 12 | ENTRYPOINT ["yab"] 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Uber Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL:=build 2 | 3 | 4 | .PHONY: build 5 | build: 6 | go build ./... 7 | 8 | .PHONY: install 9 | install: 10 | go mod download 11 | 12 | 13 | .PHONY: test 14 | test: 15 | go test -cover -timeout 30s -race ./... 16 | 17 | 18 | .PHONY: docs 19 | docs: 20 | go build 21 | # Automatically update the Usage section of README.md with --help (wrapped to 80 characters). 22 | screen -d -m bash -c 'stty cols 80 && $(CURDIR)/yab --help | python -c "import re; import sys; f = open(\"README.md\"); r = re.compile(r\"\`\`\`\nUsage:.*?\`\`\`\", re.MULTILINE|re.DOTALL); print r.sub(\"\`\`\`\n\" + sys.stdin.read() + \"\`\`\`\", f.read().strip())" | sponge README.md' 23 | # Update our manpage output and HTML pages. 24 | $(CURDIR)/yab --man-page > man/yab.1 25 | groff -man -T html man/yab.1 > man/yab.html 26 | [[ -d ../yab_ghpages ]] && cp man/yab.html ../yab_ghpages/man.html 27 | @echo "Please update gh-pages" 28 | 29 | .PHONY: test_ci 30 | test_ci: install build 31 | go test -timeout 30s -race -coverprofile=cover_temp.out -coverpkg=./... ./... 32 | grep -v "^github.com/yarpc/yab/testdata/*" cover_temp.out > cover.out 33 | go tool cover -html=cover.out -o cover.html 34 | -------------------------------------------------------------------------------- /bench_method.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "math/rand" 25 | "sync" 26 | "time" 27 | 28 | "github.com/yarpc/yab/encoding" 29 | "github.com/yarpc/yab/transport" 30 | 31 | "github.com/opentracing/opentracing-go" 32 | ) 33 | 34 | type peerTransport struct { 35 | transport.Transport 36 | peerID int 37 | } 38 | 39 | // benchmarkCaller exposes method to dispatch requests for benchmark. 40 | type benchmarkCaller interface { 41 | // Call dispatches a request using the provided transport. 42 | Call(transport.Transport) (benchmarkCallReporter, error) 43 | 44 | // CallMethodType returns the type of the RPC method invoked by `Call`. 45 | CallMethodType() encoding.MethodType 46 | } 47 | 48 | // benchmarkCallReporter exposes method to access benchmark call report like latency. 49 | type benchmarkCallReporter interface { 50 | // Latency returns the time taken to send request and receive response. 51 | Latency() time.Duration 52 | } 53 | 54 | // benchmarkStreamCallReporter exposes method to access benchmark stream call report 55 | // like stream messages send and received. 56 | type benchmarkStreamCallReporter interface { 57 | // StreamMessagesSent returns number of stream messages sent from the client. 58 | StreamMessagesSent() int 59 | 60 | // StreamMessagesReceived returns number of stream messages received from the server. 61 | StreamMessagesReceived() int 62 | } 63 | 64 | type benchmarkCallLatencyReport struct { 65 | latency time.Duration 66 | } 67 | 68 | func newBenchmarkCallLatencyReport(latency time.Duration) benchmarkCallLatencyReport { 69 | return benchmarkCallLatencyReport{latency} 70 | } 71 | 72 | func (r benchmarkCallLatencyReport) Latency() time.Duration { 73 | return r.latency 74 | } 75 | 76 | // warmTransport warms up a transport and returns it. The transport is warmed 77 | // up by making some number of requests through it. 78 | func warmTransport(b benchmarkCaller, opts TransportOptions, resolved resolvedProtocolEncoding, warmupRequests int) (transport.Transport, error) { 79 | transport, err := getTransport(opts, resolved, opentracing.NoopTracer{}) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | for i := 0; i < warmupRequests; i++ { 85 | _, err := b.Call(transport) 86 | if err != nil { 87 | return nil, err 88 | } 89 | } 90 | 91 | return transport, nil 92 | } 93 | 94 | func peerBalancer(peers []string) func(i int) (string, int) { 95 | numPeers := len(peers) 96 | startOffset := rand.Intn(numPeers) 97 | return func(i int) (string, int) { 98 | offset := (startOffset + i) % numPeers 99 | return peers[offset], offset 100 | } 101 | } 102 | 103 | // warmTransports returns n transports that have been warmed up. 104 | // No requests may fail during the warmup period. 105 | func warmTransports(b benchmarkCaller, n int, tOpts TransportOptions, resolved resolvedProtocolEncoding, warmupRequests int) ([]peerTransport, error) { 106 | peerFor := peerBalancer(tOpts.Peers) 107 | transports := make([]peerTransport, n) 108 | errs := make([]error, n) 109 | 110 | var wg sync.WaitGroup 111 | for i := range transports { 112 | wg.Add(1) 113 | go func(i int, tOpts TransportOptions) { 114 | defer wg.Done() 115 | 116 | peerHostPort, peerIndex := peerFor(i) 117 | tOpts.Peers = []string{peerHostPort} 118 | 119 | tp, err := warmTransport(b, tOpts, resolved, warmupRequests) 120 | transports[i] = peerTransport{tp, peerIndex} 121 | errs[i] = err 122 | }(i, tOpts) 123 | } 124 | 125 | wg.Wait() 126 | 127 | // If we hit any errors, return the first one. 128 | for _, err := range errs { 129 | if err != nil { 130 | return nil, err 131 | } 132 | } 133 | 134 | return transports, nil 135 | } 136 | -------------------------------------------------------------------------------- /benchmark_stream.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "io" 25 | "time" 26 | 27 | "github.com/yarpc/yab/encoding" 28 | "github.com/yarpc/yab/transport" 29 | ) 30 | 31 | // benchmarkStreamMethod benchmarks stream requests. 32 | type benchmarkStreamMethod struct { 33 | serializer encoding.Serializer 34 | streamRequest *transport.StreamRequest 35 | streamRequestMessages [][]byte 36 | opts StreamRequestOptions 37 | } 38 | 39 | // Call dispatches stream request on the provided transport. 40 | func (m benchmarkStreamMethod) Call(t transport.Transport) (benchmarkCallReporter, error) { 41 | streamIO := newStreamIOBenchmark(m.streamRequestMessages) 42 | 43 | start := time.Now() 44 | err := makeStreamRequest(t, m.streamRequest, m.serializer, streamIO, m.opts) 45 | callReport := newBenchmarkStreamCallReport(time.Since(start), streamIO.streamMessagesReceived(), streamIO.streamMessagesSent()) 46 | 47 | if err != nil { 48 | return callReport, err 49 | } 50 | 51 | // response validation is delayed to avoid consuming benchmark time for 52 | // deserializing the responses. 53 | for _, res := range streamIO.streamResponses { 54 | if err = m.serializer.CheckSuccess(&transport.Response{Body: res}); err != nil { 55 | return callReport, err 56 | } 57 | } 58 | 59 | return callReport, err 60 | } 61 | 62 | func (m benchmarkStreamMethod) CallMethodType() encoding.MethodType { 63 | return m.serializer.MethodType() 64 | } 65 | 66 | // streamIOBenchmark provides stream IO methods using the provided stream requests 67 | // and records the stream responses. 68 | type streamIOBenchmark struct { 69 | streamRequestsIdx int // current stream request iterator index 70 | streamRequests [][]byte // provided stream requests 71 | 72 | streamResponses [][]byte // recorded stream responses 73 | } 74 | 75 | func newStreamIOBenchmark(streamRequests [][]byte) *streamIOBenchmark { 76 | return &streamIOBenchmark{ 77 | streamRequests: streamRequests, 78 | } 79 | } 80 | 81 | // NextRequest returns next stream request from provided requests 82 | // returns EOF if last index has been reached. 83 | func (b *streamIOBenchmark) NextRequest() ([]byte, error) { 84 | if len(b.streamRequests) == b.streamRequestsIdx { 85 | return nil, io.EOF 86 | } 87 | 88 | // TODO: support `stream-interval` option which throttles the rate of input. 89 | req := b.streamRequests[b.streamRequestsIdx] 90 | b.streamRequestsIdx++ 91 | return req, nil 92 | } 93 | 94 | // HandleResponse records stream response. 95 | func (b *streamIOBenchmark) HandleResponse(res []byte) error { 96 | b.streamResponses = append(b.streamResponses, res) 97 | return nil 98 | } 99 | 100 | func (b *streamIOBenchmark) streamMessagesReceived() int { 101 | return len(b.streamResponses) 102 | } 103 | 104 | func (b *streamIOBenchmark) streamMessagesSent() int { 105 | return b.streamRequestsIdx 106 | } 107 | 108 | type benchmarkStreamCallReport struct { 109 | latency time.Duration 110 | streamMessagesReceived int 111 | streamMessagesSent int 112 | } 113 | 114 | func newBenchmarkStreamCallReport(latency time.Duration, streamMessagesReceived, streamMessagesSent int) benchmarkStreamCallReport { 115 | return benchmarkStreamCallReport{ 116 | latency: latency, 117 | streamMessagesReceived: streamMessagesReceived, 118 | streamMessagesSent: streamMessagesSent, 119 | } 120 | } 121 | 122 | func (r benchmarkStreamCallReport) Latency() time.Duration { 123 | return r.latency 124 | } 125 | 126 | func (r benchmarkStreamCallReport) StreamMessagesReceived() int { 127 | return r.streamMessagesReceived 128 | } 129 | 130 | func (r benchmarkStreamCallReport) StreamMessagesSent() int { 131 | return r.streamMessagesSent 132 | } 133 | -------------------------------------------------------------------------------- /benchmark_unary.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "time" 25 | 26 | "github.com/yarpc/yab/encoding" 27 | "github.com/yarpc/yab/transport" 28 | ) 29 | 30 | // benchmarkUnaryMethod benchmarks unary requests. 31 | type benchmarkUnaryMethod struct { 32 | serializer encoding.Serializer 33 | req *transport.Request 34 | } 35 | 36 | // Call dispatches unary request on the provided transport. 37 | func (m benchmarkUnaryMethod) Call(t transport.Transport) (benchmarkCallReporter, error) { 38 | start := time.Now() 39 | res, err := makeRequest(t, m.req) 40 | latency := time.Since(start) 41 | 42 | if err == nil { 43 | err = m.serializer.CheckSuccess(res) 44 | } 45 | return newBenchmarkCallLatencyReport(latency), err 46 | } 47 | 48 | func (m benchmarkUnaryMethod) CallMethodType() encoding.MethodType { 49 | return encoding.Unary 50 | } 51 | -------------------------------------------------------------------------------- /encoding/encodingerror/not_found.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT m TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encodingerror 22 | 23 | import ( 24 | "fmt" 25 | "sort" 26 | "strings" 27 | ) 28 | 29 | // NotFound error is used when a symbol isn't found (or isn't specified) 30 | // and there is a list of available symbols. 31 | type NotFound struct { 32 | Encoding string 33 | SearchType string 34 | Search string 35 | Example string 36 | LookIn string 37 | Available []string 38 | } 39 | 40 | func (e NotFound) qualifiedType() string { 41 | return e.Encoding + " " + e.SearchType 42 | } 43 | 44 | func (e NotFound) Error() string { 45 | msg := &strings.Builder{} 46 | 47 | fmt.Fprintf(msg, "no %v specified, specify %v", e.qualifiedType(), e.Example) 48 | if e.Search != "" { 49 | if e.LookIn == "" { 50 | msg.Reset() 51 | fmt.Fprintf(msg, "could not find %v %q", e.qualifiedType(), e.Search) 52 | } else { 53 | msg.Reset() 54 | fmt.Fprintf(msg, "%v %v does not contain %v %q", e.Encoding, e.LookIn, e.SearchType, e.Search) 55 | } 56 | } 57 | msg.WriteString(". ") 58 | 59 | var lookInPrefix string 60 | if e.LookIn != "" { 61 | lookInPrefix = " in " + e.LookIn 62 | } 63 | 64 | if len(e.Available) == 0 { 65 | fmt.Fprintf(msg, "No known %vs%v to list", e.qualifiedType(), lookInPrefix) 66 | return msg.String() 67 | } 68 | 69 | availablSuffix := e.SearchType 70 | if len(e.Available) > 1 { 71 | // "pluralize" the type. 72 | availablSuffix += "s" 73 | } 74 | 75 | sort.Strings(e.Available) 76 | 77 | fmt.Fprintf(msg, "Available %v %v%v:\n\t%v", e.Encoding, availablSuffix, lookInPrefix, strings.Join(e.Available, "\n\t")) 78 | return msg.String() 79 | } 80 | -------------------------------------------------------------------------------- /encoding/encodingerror/not_found_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT m TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encodingerror 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestNotFound(t *testing.T) { 30 | tests := []struct { 31 | msg string 32 | err error 33 | errMsg string 34 | }{ 35 | { 36 | msg: "service not specified, none available", 37 | err: NotFound{ 38 | Encoding: "Thrift", 39 | SearchType: "service", 40 | Example: "--method Service::Method", 41 | }, 42 | errMsg: "no Thrift service specified, specify --method Service::Method. No known Thrift services to list", 43 | }, 44 | { 45 | msg: "specified not specified, some available", 46 | err: NotFound{ 47 | Encoding: "gRPC", 48 | SearchType: "service", 49 | Example: "--method Service::Method", 50 | Search: "", 51 | Available: []string{"Foo", "Bar"}, 52 | }, 53 | errMsg: "no gRPC service specified, specify --method Service::Method. Available gRPC services:\n\tBar\n\tFoo", 54 | }, 55 | { 56 | msg: "service not found, none available", 57 | err: NotFound{ 58 | Encoding: "gRPC", 59 | SearchType: "service", 60 | Example: "--method Service::Method", 61 | Search: "Baz", 62 | }, 63 | errMsg: `could not find gRPC service "Baz". No known gRPC services to list`, 64 | }, 65 | { 66 | msg: "service not found, single available", 67 | err: NotFound{ 68 | Encoding: "Thrift", 69 | SearchType: "service", 70 | Example: "--method Service::Method", 71 | Search: "Baz", 72 | Available: []string{"Foo"}, 73 | }, 74 | errMsg: `could not find Thrift service "Baz". ` + "Available Thrift service:\n\tFoo", 75 | }, 76 | { 77 | msg: "method not specified, none available", 78 | err: NotFound{ 79 | Encoding: "Thrift", 80 | SearchType: "method", 81 | LookIn: `service "Bar"`, 82 | Example: "--method Service::Method", 83 | }, 84 | errMsg: `no Thrift method specified, specify --method Service::Method. No known Thrift methods in service "Bar" to list`, 85 | }, 86 | { 87 | msg: "method not specified, some available", 88 | err: NotFound{ 89 | Encoding: "Thrift", 90 | SearchType: "method", 91 | LookIn: `service "Bar"`, 92 | Example: "--method Service::Method", 93 | Available: []string{"m1", "m2"}, 94 | }, 95 | errMsg: `no Thrift method specified, specify --method Service::Method. Available Thrift methods in service "Bar":` + "\n\tm1\n\tm2", 96 | }, 97 | { 98 | msg: "method not found, some available", 99 | err: NotFound{ 100 | Encoding: "Thrift", 101 | SearchType: "method", 102 | LookIn: `service "Bar"`, 103 | Example: "--method Service::Method", 104 | Search: "echo", 105 | Available: []string{"m1", "m2"}, 106 | }, 107 | errMsg: `Thrift service "Bar" does not contain method "echo". Available Thrift methods in service "Bar":` + "\n\tm1\n\tm2", 108 | }, 109 | } 110 | 111 | for _, tt := range tests { 112 | t.Run(tt.msg, func(t *testing.T) { 113 | assert.EqualError(t, tt.err, tt.errMsg) 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /encoding/inputdecoder/decoder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package inputdecoder 22 | 23 | import ( 24 | "bufio" 25 | "encoding/json" 26 | "io" 27 | 28 | "gopkg.in/yaml.v2" 29 | ) 30 | 31 | // Decoder interface exposes method for reading multiple input requests 32 | type Decoder interface { 33 | // NextYAMLBytes returns YAML marshaled bytes of the next request body 34 | // It returns io.EOF when there are no more requests 35 | NextYAMLBytes() ([]byte, error) 36 | } 37 | 38 | // jsonInputDecoder parses multiple JSON objects from given reader 39 | // JSON objects can be delimited by space or newline 40 | type jsonInputDecoder struct{ dec *json.Decoder } 41 | 42 | func (r *jsonInputDecoder) NextYAMLBytes() ([]byte, error) { 43 | if !r.dec.More() { 44 | return nil, io.EOF 45 | } 46 | 47 | var v json.RawMessage 48 | err := r.dec.Decode(&v) 49 | return []byte(v), err 50 | } 51 | 52 | // yamlInputDecoder parses multiple YAML objects from the given reader 53 | // consecutive YAML objects must be delimited by `---` 54 | type yamlInputDecoder struct{ dec *yaml.Decoder } 55 | 56 | func (r *yamlInputDecoder) NextYAMLBytes() ([]byte, error) { 57 | var v interface{} 58 | if err := r.dec.Decode(&v); err != nil { 59 | return nil, err 60 | } 61 | 62 | return yaml.Marshal(v) 63 | } 64 | 65 | // isJSONInput assumes the input is JSON compatible if the initial 66 | // byte read is `{` 67 | func isJSONInput(r *bufio.Reader) (bool, error) { 68 | b, err := r.ReadByte() 69 | if err != nil { 70 | return false, err 71 | } 72 | 73 | return b == '{', r.UnreadByte() 74 | } 75 | 76 | // New detects the input encoding type, returns either 77 | // json or yaml request decoder 78 | func New(reader io.Reader) (Decoder, error) { 79 | bufReader := bufio.NewReader(reader) 80 | isJSON, err := isJSONInput(bufReader) 81 | if err != nil && err != io.EOF { 82 | return nil, err 83 | } 84 | 85 | if isJSON { 86 | return &jsonInputDecoder{json.NewDecoder(bufReader)}, nil 87 | } 88 | 89 | return &yamlInputDecoder{yaml.NewDecoder(bufReader)}, nil 90 | } 91 | -------------------------------------------------------------------------------- /encoding/meta.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encoding 22 | 23 | import ( 24 | "fmt" 25 | "sync" 26 | 27 | "go.uber.org/thriftrw/compile" 28 | ) 29 | 30 | // _meta_thrift is the meta.thrift file copied from https://github.com/uber/tchannel/blob/master/thrift/meta.thrift 31 | const _metaThrift = ` 32 | struct HealthStatus { 33 | 1: required bool ok 34 | 2: optional string message 35 | } 36 | 37 | typedef string filename 38 | 39 | struct ThriftIDLs { 40 | // map: filename -> contents 41 | 1: required map idls 42 | // the entry IDL that imports others 43 | 2: required filename entryPoint 44 | } 45 | 46 | struct VersionInfo { 47 | // short string naming the implementation language 48 | 1: required string language 49 | // language-specific version string representing runtime or build chain 50 | 2: required string language_version 51 | // semver version indicating the version of the tchannel library 52 | 3: required string version 53 | } 54 | 55 | service Meta { 56 | HealthStatus health() 57 | ThriftIDLs thriftIDL() 58 | VersionInfo versionInfo() 59 | } 60 | ` 61 | 62 | const ( 63 | metaService = "Meta" 64 | healthMethod = "health" 65 | ) 66 | 67 | var ( 68 | metaModuleOnce sync.Once 69 | metaModule *compile.Module 70 | ) 71 | 72 | type metaFS struct{} 73 | 74 | func (metaFS) Read(_ string) ([]byte, error) { 75 | return []byte(_metaThrift), nil 76 | } 77 | 78 | func (metaFS) Abs(p string) (string, error) { 79 | return p, nil 80 | } 81 | 82 | func getMetaService() *compile.ServiceSpec { 83 | metaModuleOnce.Do(func() { 84 | var err error 85 | metaModule, err = compile.Compile("meta.thrift", compile.Filesystem(metaFS{})) 86 | if err != nil { 87 | panic(fmt.Sprintf("failed to parse embedded meta.thrift: %v", err)) 88 | } 89 | }) 90 | 91 | return metaModule.Services[metaService] 92 | } 93 | 94 | func getHealthSpec() (string, *compile.FunctionSpec) { 95 | return metaService + "::" + healthMethod, getMetaService().Functions[healthMethod] 96 | } 97 | -------------------------------------------------------------------------------- /encoding/meta_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encoding 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestMetaSpec(t *testing.T) { 31 | assert.NotPanics(t, func() { 32 | spec := getMetaService() 33 | assert.NotNil(t, spec, "Meta service spec is nil") 34 | }, "Failed to get Meta service") 35 | 36 | assert.NotPanics(t, func() { 37 | name, spec := getHealthSpec() 38 | assert.Equal(t, "Meta::health", name, "Method name mismatch") 39 | require.NotNil(t, spec, "Got nil health spec") 40 | assert.Equal(t, 0, len(spec.ArgsSpec), "Health method") 41 | }, "Failed to get health spec") 42 | } 43 | -------------------------------------------------------------------------------- /encoding/protobuf_health.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/golang/protobuf/jsonpb" 8 | "github.com/golang/protobuf/proto" 9 | "github.com/yarpc/yab/transport" 10 | "go.uber.org/yarpc/pkg/procedure" 11 | "google.golang.org/grpc/health/grpc_health_v1" 12 | ) 13 | 14 | type protoHealthSerializer struct { 15 | serviceName string 16 | } 17 | 18 | func (p protoHealthSerializer) Encoding() Encoding { 19 | return Protobuf 20 | } 21 | 22 | func (protoHealthSerializer) MethodType() MethodType { 23 | return Unary 24 | } 25 | 26 | func (p protoHealthSerializer) Request(body []byte) (*transport.Request, error) { 27 | if len(body) > 0 { 28 | return nil, errors.New("cannot specify --health and a request body") 29 | } 30 | bytes, err := proto.Marshal(&grpc_health_v1.HealthCheckRequest{Service: p.serviceName}) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &transport.Request{ 35 | Method: procedure.ToName("grpc.health.v1.Health", "Check"), 36 | Body: bytes, 37 | }, nil 38 | } 39 | 40 | func (p protoHealthSerializer) Response(body *transport.Response) (interface{}, error) { 41 | var msg grpc_health_v1.HealthCheckResponse 42 | if err := proto.Unmarshal(body.Body, &msg); err != nil { 43 | return nil, err 44 | } 45 | m := jsonpb.Marshaler{} 46 | str, err := m.MarshalToString(&msg) 47 | if err != nil { 48 | return nil, err 49 | } 50 | var unmarshaledJSON json.RawMessage 51 | if err = json.Unmarshal([]byte(str), &unmarshaledJSON); err != nil { 52 | return nil, err 53 | } 54 | return unmarshaledJSON, nil 55 | } 56 | 57 | func (p protoHealthSerializer) CheckSuccess(body *transport.Response) error { 58 | _, err := p.Response(body) 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /encoding/protobuf_health_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/yarpc/yab/transport" 10 | ) 11 | 12 | func TestProtobufHealth(t *testing.T) { 13 | s := &protoHealthSerializer{} 14 | assert.Equal(t, Protobuf, s.Encoding()) 15 | } 16 | 17 | func TestProtobufHealthRequest(t *testing.T) { 18 | tests := []struct { 19 | desc string 20 | serviceName string 21 | reqBody []byte 22 | bsOut []byte 23 | errMsg string 24 | }{ 25 | { 26 | desc: "empty", 27 | bsOut: []byte{}, 28 | }, 29 | { 30 | desc: "no input", 31 | serviceName: "x", 32 | reqBody: []byte("x"), 33 | errMsg: "cannot specify --health and a request body", 34 | }, 35 | { 36 | desc: "pass", 37 | serviceName: "x", 38 | bsOut: []byte{0xa, 0x1, 0x78}, 39 | }, 40 | { 41 | desc: "fail", 42 | serviceName: string([]byte{0xc3, 0x28}), 43 | errMsg: "invalid UTF-8", 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | t.Run(tt.desc, func(t *testing.T) { 49 | serializer := &protoHealthSerializer{serviceName: tt.serviceName} 50 | got, err := serializer.Request(tt.reqBody) 51 | if tt.errMsg == "" { 52 | assert.NoError(t, err, "%v", tt.desc) 53 | require.NotNil(t, got, "%v: Invalid request") 54 | assert.Equal(t, tt.bsOut, got.Body) 55 | } else { 56 | assert.Nil(t, got, "%v: Error cases should not return any bytes", tt.desc) 57 | require.Error(t, err, "%v", tt.desc) 58 | assert.Contains(t, err.Error(), tt.errMsg, "%v: invalid error", tt.desc) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestProtobufHealthResponse(t *testing.T) { 65 | tests := []struct { 66 | desc string 67 | reqBody []byte 68 | outAsJSON string 69 | errMsg string 70 | }{ 71 | { 72 | desc: "pass", 73 | reqBody: nil, 74 | outAsJSON: "{}", 75 | }, 76 | { 77 | desc: "fail", 78 | reqBody: []byte{0x1, 0x2}, 79 | errMsg: "invalid field number", 80 | }, 81 | } 82 | 83 | for _, tt := range tests { 84 | t.Run(tt.desc, func(t *testing.T) { 85 | serializer := &protoHealthSerializer{} 86 | response := &transport.Response{ 87 | Body: tt.reqBody, 88 | } 89 | got, err := serializer.Response(response) 90 | if tt.errMsg == "" { 91 | assert.NoError(t, err, "%v", tt.desc) 92 | assert.NotNil(t, got, "%v: Invalid request") 93 | r, err := json.Marshal(got) 94 | assert.NoError(t, err) 95 | assert.JSONEq(t, tt.outAsJSON, string(r)) 96 | 97 | err = serializer.CheckSuccess(response) 98 | assert.NoError(t, err) 99 | } else { 100 | assert.Nil(t, got, "%v: Error cases should not return any bytes", tt.desc) 101 | require.Error(t, err, "%v", tt.desc) 102 | assert.Contains(t, err.Error(), tt.errMsg, "%v: invalid error", tt.desc) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /encoding/yaml_to_thrift_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package encoding 22 | 23 | import ( 24 | "io/ioutil" 25 | "strings" 26 | "testing" 27 | 28 | "gopkg.in/yaml.v2" 29 | 30 | "path/filepath" 31 | 32 | "github.com/stretchr/testify/assert" 33 | "github.com/stretchr/testify/require" 34 | "github.com/yarpc/yab/thrift" 35 | "github.com/yarpc/yab/transport" 36 | "go.uber.org/thriftrw/compile" 37 | ) 38 | 39 | func getSpec(t *testing.T, thriftFile string) compile.TypeSpec { 40 | module, err := thrift.Parse(thriftFile) 41 | require.NoError(t, err, "Failed to parse %v: %v", thriftFile, err) 42 | 43 | spec, err := module.LookupType("S") 44 | require.NoError(t, err, "Failed to find type s in thrift file") 45 | return spec 46 | } 47 | 48 | func getSerializer(typeSpec compile.TypeSpec) thriftSerializer { 49 | // Not inline to avoid go vet unkeyed literal error. 50 | argSpec := compile.ArgsSpec{{ 51 | ID: 0, 52 | Name: "arg", 53 | Type: typeSpec, 54 | Required: true, 55 | }} 56 | return thriftSerializer{ 57 | methodName: "dummy", 58 | spec: &compile.FunctionSpec{ 59 | Name: "dummy", 60 | ArgsSpec: argSpec, 61 | ResultSpec: &compile.ResultSpec{ 62 | ReturnType: typeSpec, 63 | }, 64 | }, 65 | } 66 | } 67 | 68 | func TestYAMLToThrift(t *testing.T) { 69 | const testDir = "../testdata/yamltothrift/" 70 | 71 | files, err := ioutil.ReadDir(testDir) 72 | require.NoError(t, err, "Failed to read test directory %v: %v", testDir, err) 73 | 74 | for _, f := range files { 75 | inFile := filepath.Join(testDir, f.Name()) 76 | var noSuffix string 77 | switch { 78 | case strings.HasSuffix(inFile, ".json"): 79 | noSuffix = strings.TrimSuffix(inFile, ".json") 80 | case strings.HasSuffix(inFile, ".yaml"): 81 | noSuffix = strings.TrimSuffix(inFile, ".yaml") 82 | default: 83 | continue 84 | } 85 | 86 | thriftFile := noSuffix + ".thrift" 87 | outFile := noSuffix + ".out" 88 | inContents, err := ioutil.ReadFile(inFile) 89 | require.NoError(t, err, "Failed to read input file: %v", inFile) 90 | 91 | serializer := getSerializer(getSpec(t, thriftFile)) 92 | req, err := serializer.Request(inContents) 93 | require.NoError(t, err, "Failed to get request") 94 | 95 | res := &transport.Response{Body: req.Body} 96 | got, err := serializer.Response(res) 97 | require.NoError(t, err, "Failed to convert response") 98 | gotYAML, err := yaml.Marshal(got) 99 | require.NoError(t, err, "Failed to marshal output to YAML") 100 | 101 | want, err := ioutil.ReadFile(outFile) 102 | require.NoError(t, err, "Failed to read expected output file") 103 | 104 | assert.Equal(t, string(want), string(gotYAML), "Output did not match :%v", outFile) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /etc/bash_completion.d/yab: -------------------------------------------------------------------------------- 1 | # bash completion for yab executable 2 | # based on https://godoc.org/github.com/jessevdk/go-flags#hdr-Completion 3 | # depends on https://salsa.debian.org/debian/bash-completion 4 | 5 | 6 | have yab && 7 | _yab() 8 | { 9 | # All arguments except the first one 10 | args=("${COMP_WORDS[@]:1:$COMP_CWORD}") 11 | 12 | # Only split on newlines 13 | local IFS=$'\n' 14 | 15 | # Call completion (note that the first element of COMP_WORDS is 16 | # the executable itself) 17 | COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) 18 | return 0 19 | } && 20 | complete -F _yab yab 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yarpc/yab 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7 7 | github.com/cactus/go-statsd-client/v5 v5.0.0 8 | github.com/casimir/xdg-go v0.0.0-20160329195404-372ccc2180da 9 | github.com/ghodss/yaml v1.0.0 10 | github.com/golang/protobuf v1.4.3 11 | github.com/jessevdk/go-flags v1.5.0 12 | github.com/jhump/protoreflect v0.0.0-20180908113807-a84568470d8a 13 | github.com/opentracing/opentracing-go v1.2.0 14 | github.com/stretchr/testify v1.7.1 15 | github.com/uber/jaeger-client-go v2.30.0+incompatible 16 | github.com/uber/tchannel-go v1.32.1 17 | go.uber.org/atomic v1.9.0 18 | go.uber.org/multierr v1.8.0 19 | go.uber.org/thriftrw v1.29.2 20 | go.uber.org/yarpc v1.60.0 21 | go.uber.org/zap v1.21.0 22 | golang.org/x/net v0.0.0-20220403103023-749bd193bc2b 23 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 24 | google.golang.org/grpc v1.40.1 25 | gopkg.in/yaml.v2 v2.4.0 26 | ) 27 | 28 | require ( 29 | github.com/BurntSushi/toml v0.3.1 // indirect 30 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 31 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 32 | github.com/beorn7/perks v1.0.1 // indirect 33 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 34 | github.com/davecgh/go-spew v1.1.1 // indirect 35 | github.com/gogo/googleapis v1.3.2 // indirect 36 | github.com/gogo/protobuf v1.3.1 // indirect 37 | github.com/gogo/status v1.1.0 // indirect 38 | github.com/google/go-cmp v0.5.4 // indirect 39 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 40 | github.com/pkg/errors v0.9.1 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/prometheus/client_golang v1.4.1 // indirect 43 | github.com/prometheus/client_model v0.2.0 // indirect 44 | github.com/prometheus/common v0.9.1 // indirect 45 | github.com/prometheus/procfs v0.0.9 // indirect 46 | github.com/uber-go/mapdecode v1.0.0 // indirect 47 | github.com/uber-go/tally v3.3.15+incompatible // indirect 48 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 49 | go.uber.org/net/metrics v1.3.0 // indirect 50 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect 51 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 52 | golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb // indirect 53 | golang.org/x/text v0.3.7 // indirect 54 | golang.org/x/tools v0.1.10 // indirect 55 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 56 | google.golang.org/protobuf v1.25.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 58 | honnef.co/go/tools v0.0.1-2019.2.3 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /grpc_server_util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/health" 10 | "google.golang.org/grpc/health/grpc_health_v1" 11 | "google.golang.org/grpc/reflection" 12 | ) 13 | 14 | const _grpcService = "yab-grpc-test" 15 | 16 | type grpcServer struct { 17 | ln net.Listener 18 | server *grpc.Server 19 | healthHandler *health.Server 20 | } 21 | 22 | func newGRPCServer(t *testing.T) *grpcServer { 23 | ln, err := net.Listen("tcp", "127.0.0.1:0") 24 | require.NoError(t, err, "Failed to create TCP listener") 25 | 26 | server := grpc.NewServer() 27 | healthHandler := health.NewServer() 28 | 29 | grpc_health_v1.RegisterHealthServer(server, healthHandler) 30 | reflection.Register(server) 31 | s := &grpcServer{ln, server, healthHandler} 32 | s.SetHealth(true /* serving */) 33 | 34 | go server.Serve(ln) 35 | return s 36 | } 37 | 38 | func (s *grpcServer) HostPort() string { 39 | return s.ln.Addr().String() 40 | } 41 | 42 | func (s *grpcServer) SetHealth(serving bool) { 43 | status := grpc_health_v1.HealthCheckResponse_NOT_SERVING 44 | if serving { 45 | status = grpc_health_v1.HealthCheckResponse_SERVING 46 | } 47 | 48 | s.healthHandler.SetServingStatus(_grpcService, status) 49 | } 50 | 51 | func (s *grpcServer) Stop() { 52 | s.server.Stop() 53 | } 54 | -------------------------------------------------------------------------------- /internal/thrifttest/thrifttest.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package thrifttest contains utilities to help test THrift serialization. 22 | package thrifttest 23 | 24 | import ( 25 | "os" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/require" 29 | "go.uber.org/thriftrw/compile" 30 | ) 31 | 32 | // DummyFS is an in-memory implementation of the Filesystem interface. 33 | type DummyFS map[string][]byte 34 | 35 | // Read returns the contents for the specified file. 36 | func (fs DummyFS) Read(filename string) ([]byte, error) { 37 | if contents, ok := fs[filename]; ok { 38 | return contents, nil 39 | } 40 | return nil, os.ErrNotExist 41 | } 42 | 43 | // Abs returns the absolute path for the specified file. 44 | // The dummy implementation always returns the original path. 45 | func (DummyFS) Abs(filename string) (string, error) { 46 | return filename, nil 47 | } 48 | 49 | // Parse parses the given file contents as a Thrift file with no dependencies. 50 | func Parse(t *testing.T, contents string) *compile.Module { 51 | fs := DummyFS{ 52 | "file.thrift": []byte(contents), 53 | } 54 | compiled, err := compile.Compile("file.thrift", compile.Filesystem(fs)) 55 | require.NoError(t, err, "Failed to compile thrift file:\n%s", contents) 56 | return compiled 57 | } 58 | -------------------------------------------------------------------------------- /internal/yamlalias/wrap.go: -------------------------------------------------------------------------------- 1 | // Package yamlalias adds support for aliases when parsing YAML. 2 | // 3 | // Aliases are specified as a comma-separated list specified via 4 | // "yaml-aliases" struct tag. 5 | // 6 | // Example: 7 | // 8 | // type Config struct { 9 | // UserName string `yaml:"username" yaml-aliases:"userName,user-name"` 10 | // } 11 | // 12 | // This will parse the following YAML examples in the same way: 13 | // {"username": "John Doe"} 14 | // {"userName": "John Doe"} 15 | // {"user-name": "John Doe"} 16 | // 17 | // Note: It only adds aliases for top-level fields in the struct. 18 | package yamlalias 19 | 20 | import ( 21 | "fmt" 22 | "reflect" 23 | "strings" 24 | 25 | "gopkg.in/yaml.v2" 26 | ) 27 | 28 | // Unmarshal is a wrapper for yaml.Unmarshal but adds 29 | // support for any yaml-aliases specified in out. 30 | func Unmarshal(in []byte, out interface{}) error { 31 | return yaml.Unmarshal(in, addAliases(out)) 32 | } 33 | 34 | // UnmarshalStrict is a wrapper for yaml.UnmarshalStrict but adds 35 | // support for any yaml-aliases specified in out. 36 | func UnmarshalStrict(in []byte, out interface{}) error { 37 | return yaml.UnmarshalStrict(in, addAliases(out)) 38 | } 39 | 40 | // addAliases generates a new struct for unmarshalling that contains all exported 41 | // fields from the passed in struct, but also adds additional fields for any 42 | // aliases. 43 | // 44 | // Given a struct such as: 45 | // 46 | // type Config struct { 47 | // UserName string `yaml:"username" yaml-aliases:"userName,user-name"` 48 | // TTL time.Duration `yaml:"ttl"` 49 | // } 50 | // 51 | // It will create a new struct with a pointer field for each field in the 52 | // original struct and a new field for each alias. 53 | // 54 | // struct { 55 | // UserName *string `yaml:"username" yaml-aliases:"userName,user-name"` 56 | // UserNameYamlAlias1 *string `yaml:"userName"` 57 | // UserNameYamlAlias2 *string `yaml:"user-name"` 58 | // TTL *time.Duration `yaml:"ttl"` 59 | // } 60 | // 61 | // A value of that struct is returned with the pointer fields pointing to 62 | // fields of the original struct. 63 | // 64 | // out.UserName = &dest.UserName 65 | // out.UserNameYamlAlias1 = &dest.UserName 66 | // out.UserNameYamlAlias2 = &dest.UserName 67 | // out.TTL = &dest.TTL 68 | // return out 69 | // 70 | // The YAML library respects existing pointers when unmarshalling, and 71 | // does not replace them: 72 | // https://gist.github.com/prashantv/fa4f92b4b95f936d68495be250ed3506 73 | func addAliases(dest interface{}) interface{} { 74 | rv := reflect.ValueOf(dest).Elem() 75 | rt := rv.Type() 76 | 77 | fields := make([]reflect.StructField, 0, rt.NumField()) 78 | dests := make([]reflect.Value, 0, rt.NumField()) 79 | for i := 0; i < rt.NumField(); i++ { 80 | f := rt.Field(i) 81 | 82 | // Ignore unexported fields. 83 | if f.PkgPath != "" { 84 | continue 85 | } 86 | 87 | // Add a pointer field that matches the original field in the struct. 88 | fields = append(fields, reflect.StructField{ 89 | Name: f.Name, 90 | Type: reflect.PtrTo(f.Type), 91 | Tag: f.Tag, 92 | }) 93 | dests = append(dests, rv.Field(i).Addr()) 94 | 95 | allAliases, ok := f.Tag.Lookup("yaml-aliases") 96 | if !ok { 97 | // No aliases specified. No other fields to add. 98 | continue 99 | } 100 | 101 | aliases := strings.Split(allAliases, ",") 102 | for j, alias := range aliases { 103 | // Add a new field that is a pointer to the original field. 104 | // We generate a name that is unlikely to clash. Clashes will cause panics. 105 | fields = append(fields, reflect.StructField{ 106 | Name: fmt.Sprintf("%vYamlAlias%v", f.Name, j), 107 | Type: reflect.PtrTo(f.Type), 108 | Tag: reflect.StructTag(fmt.Sprintf(`yaml:%q`, alias)), 109 | }) 110 | dests = append(dests, rv.Field(i).Addr()) 111 | } 112 | } 113 | 114 | // The struct we generated has pointers to the original fields. 115 | // Set all the pointers to the fields in the original struct. 116 | generated := reflect.StructOf(fields) 117 | out := reflect.New(generated) 118 | for i, dest := range dests { 119 | out.Elem().Field(i).Set(dest) 120 | } 121 | 122 | return out.Interface() 123 | } 124 | -------------------------------------------------------------------------------- /internal/yamlalias/wrap_test.go: -------------------------------------------------------------------------------- 1 | package yamlalias 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func stringPtr(s string) *string { 13 | return &s 14 | } 15 | 16 | func TestWrap(t *testing.T) { 17 | type Simple struct { 18 | Foo string `yaml:"foo" yaml-aliases:"bar,baz"` 19 | } 20 | 21 | type ManyTypes struct { 22 | Str string `yaml-aliases:"string"` 23 | Int int `yaml-aliases:"Int"` 24 | Slice []string `yaml-aliases:"list"` 25 | Map map[string]string `yaml-aliases:"dict"` 26 | } 27 | 28 | type AliasToPtr struct { 29 | Foo *string `yaml:"foo" yaml-aliases:"bar"` 30 | } 31 | 32 | tests := []struct { 33 | msg string 34 | data string 35 | want interface{} 36 | }{ 37 | { 38 | msg: "No aliases", 39 | data: `{"foo": "foo", "bar": "bar"}`, 40 | want: &struct { 41 | Foo string 42 | Bar string 43 | }{"foo", "bar"}, 44 | }, 45 | { 46 | msg: "set field by normal name", 47 | data: `{"foo": "foo"}`, 48 | want: &Simple{"foo"}, 49 | }, 50 | { 51 | msg: "set field by first alias", 52 | data: `{"bar": "bar"}`, 53 | want: &Simple{"bar"}, 54 | }, 55 | { 56 | msg: "set field by second alias", 57 | data: `{"baz": "baz"}`, 58 | want: &Simple{"baz"}, 59 | }, 60 | { 61 | msg: "set field with multiple aliases, last one wins", 62 | data: `{"bar": "bar", "foo": "foo", "baz": "baz"}`, 63 | want: &Simple{"baz"}, 64 | }, 65 | { 66 | msg: "pointer field that is not set", 67 | data: `{}`, 68 | want: &AliasToPtr{}, 69 | }, 70 | { 71 | msg: "pointer field set directly", 72 | data: `{"foo": "foo"}`, 73 | want: &AliasToPtr{stringPtr("foo")}, 74 | }, 75 | { 76 | msg: "pointer field that is set by alias", 77 | data: `{"bar": "bar"}`, 78 | want: &AliasToPtr{stringPtr("bar")}, 79 | }, 80 | { 81 | msg: "Unexported fields are ignored", 82 | want: &struct { 83 | Foo string `yaml:"foo" yaml-aliases:"bar"` 84 | bar string `yaml:"bar" yaml-aliases:"baz"` 85 | }{ 86 | Foo: "bar", 87 | bar: "", 88 | }, 89 | data: `{"bar": "bar"}`, 90 | }, 91 | { 92 | msg: "Alias with different case", 93 | want: &struct { 94 | Foo string `yaml:"foo" yaml-aliases:"Foo"` 95 | }{ 96 | Foo: "Foo", 97 | }, 98 | data: `{"Foo": "Foo"}`, 99 | }, 100 | { 101 | msg: "Alias with dashes", 102 | want: &struct { 103 | Foo string `yaml:"foo" yaml-aliases:"foo-bar"` 104 | }{ 105 | Foo: "foo-bar", 106 | }, 107 | data: `{"foo-bar": "foo-bar"}`, 108 | }, 109 | } 110 | 111 | for _, tt := range tests { 112 | t.Run(tt.msg, func(t *testing.T) { 113 | // To allow use of anonymous structs, we use reflection to create 114 | // a new empty struct. 115 | v := reflect.New(reflect.TypeOf(tt.want).Elem()).Interface() 116 | require.NoError(t, Unmarshal([]byte(tt.data), v)) 117 | assert.Equal(t, tt.want, v) 118 | }) 119 | } 120 | } 121 | 122 | func TestWrapPointerField(t *testing.T) { 123 | var dest string 124 | type StructWithPointer struct { 125 | Foo *string `yaml:"foo" yaml-aliases:"bar"` 126 | } 127 | 128 | s := StructWithPointer{Foo: &dest} 129 | require.NoError(t, yaml.Unmarshal([]byte(`{"foo": "initial"}`), &s), "failed to unmarshal") 130 | assert.Equal(t, "initial", dest, "dest expected to be updated") 131 | 132 | require.NoError(t, Unmarshal([]byte(`{"bar": "updated"}`), &s), "failed to unmarshal with alias") 133 | assert.Equal(t, "updated", dest, "expected dest to be updated via alias") 134 | } 135 | 136 | func TestWrapInvalid(t *testing.T) { 137 | tests := []struct { 138 | msg string 139 | t interface{} 140 | }{ 141 | { 142 | msg: "Alias clashes with other field", 143 | t: &struct { 144 | Foo string `yaml:"foo" yaml-aliases:"bar,baz"` 145 | Bar string `yaml:"bar"` 146 | }{}, 147 | }, 148 | { 149 | msg: "Alias clashes with another alias", 150 | t: &struct { 151 | Foo string `yaml:"foo" yaml-aliases:"baz"` 152 | Bar string `yaml:"bar" yaml-aliases:"baz"` 153 | }{}, 154 | }, 155 | } 156 | 157 | for _, tt := range tests { 158 | t.Run(tt.msg, func(t *testing.T) { 159 | assert.Panics(t, func() { 160 | Unmarshal([]byte(`{}`), &tt.t) 161 | }) 162 | }) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/yarpc/yab/ratelimit" 7 | 8 | "go.uber.org/atomic" 9 | ) 10 | 11 | // Run represents a single run that is limited by either 12 | // a number of requests, or can be stopped halfway. 13 | type Run struct { 14 | unlimited atomic.Bool 15 | requestsLeft atomic.Int64 16 | limiter ratelimit.Limiter 17 | cancel chan struct{} 18 | cancelled atomic.Bool 19 | } 20 | 21 | // New returns 22 | func New(maxRequests, rps int, maxDuration time.Duration) *Run { 23 | limiter := ratelimit.NewInfinite() 24 | if rps > 0 { 25 | limiter = ratelimit.New(rps) 26 | } 27 | 28 | r := &Run{ 29 | unlimited: *atomic.NewBool(maxRequests == 0), 30 | requestsLeft: *atomic.NewInt64(int64(maxRequests)), 31 | limiter: limiter, 32 | cancel: make(chan struct{}), 33 | } 34 | 35 | if maxDuration > 0 { 36 | time.AfterFunc(maxDuration, r.Stop) 37 | } 38 | 39 | return r 40 | } 41 | 42 | // More returns whether more requests can be started or whether we have 43 | // reached some limit. If more requests can be made, it blocks until the rate 44 | // limiter allows another request. 45 | func (r *Run) More() bool { 46 | if r.unlimited.Load() { 47 | r.limiter.Take(r.cancel) 48 | return true 49 | } 50 | 51 | if r.requestsLeft.Load() >= 0 { 52 | r.limiter.Take(r.cancel) 53 | } 54 | return r.requestsLeft.Dec() >= 0 55 | } 56 | 57 | // Stop will ensure that all future calls to More return false. 58 | func (r *Run) Stop() { 59 | if r.cancelled.Swap(true) { 60 | // Already stopped 61 | return 62 | } 63 | r.requestsLeft.Store(0) 64 | r.unlimited.Store(false) 65 | close(r.cancel) 66 | } 67 | -------------------------------------------------------------------------------- /limiter/limiter_test.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/uber/tchannel-go/testutils" 10 | ) 11 | 12 | func TestSerial(t *testing.T) { 13 | run := New(1000 /* maxRequests */, 100000 /* rps */, time.Second) 14 | for i := 0; i < 1000; i++ { 15 | assert.True(t, run.More(), "Request %v should succeed", i) 16 | } 17 | for i := 0; i < 1000; i++ { 18 | assert.False(t, run.More(), "Request %v should fail", i) 19 | } 20 | } 21 | 22 | func TestRateLimited(t *testing.T) { 23 | run := New(1000 /* maxRequests */, 100 /* rps */, time.Second) 24 | assert.True(t, run.More(), "First request should succeed") 25 | started := time.Now() 26 | assert.True(t, run.More(), "Second request should succeed") 27 | elapsed := time.Since(started) 28 | 29 | // Since RPS is 100, we should only execute one call every 10ms. 30 | // Because of timing + call overhead, we make a safe asseertion for > 5ms 31 | // but < 15ms. 32 | assert.True(t, elapsed > 5*time.Millisecond && elapsed < 15*time.Millisecond, 33 | "Second More elapsed is unexpected, expected 5ms < %v < 15ms", elapsed) 34 | } 35 | 36 | func TestParallel(t *testing.T) { 37 | run := New(1000 /* maxRequests */, 100000 /* rps */, time.Second) 38 | 39 | var wg sync.WaitGroup 40 | for i := 0; i < 10; i++ { 41 | wg.Add(1) 42 | go func() { 43 | defer wg.Done() 44 | for i := 0; i < 100; i++ { 45 | assert.True(t, run.More(), "First 100 requests in each goroutine should succeed, failed at %v", i) 46 | } 47 | }() 48 | } 49 | 50 | wg.Wait() 51 | assert.False(t, run.More(), "Requests should fail after 1000 parallel requests") 52 | } 53 | 54 | func TestStop(t *testing.T) { 55 | run := New(0 /* maxRequests */, 0 /* rps */, 0 /* maxDuration */) 56 | 57 | for i := 0; i < 100; i++ { 58 | assert.True(t, run.More(), "Before Stop() should succeed, iteration %v", i) 59 | } 60 | run.Stop() 61 | for i := 0; i < 1000; i++ { 62 | assert.False(t, run.More(), "After Stop() should fail") 63 | } 64 | 65 | // Make sure we can stop multiple times 66 | run.Stop() 67 | run.Stop() 68 | assert.False(t, run.More(), "After Stop() should fail") 69 | } 70 | 71 | func TestTimeout(t *testing.T) { 72 | run := New(1000 /* maxRequests */, 1000 /* rps */, time.Millisecond) 73 | assert.True(t, run.More(), "Succeed within the timeout") 74 | time.Sleep(5 * time.Millisecond) 75 | assert.False(t, run.More(), "Fail after the timeout") 76 | } 77 | 78 | func TestUnlimitedRequests(t *testing.T) { 79 | timeout := testutils.Timeout(100 * time.Millisecond) 80 | run := New(0 /* maxRequests */, 1000 /* rps */, timeout) 81 | for i := 0; i < 5; i++ { 82 | assert.True(t, run.More(), "Unlimited should suceed till timeout") 83 | } 84 | time.Sleep(timeout) 85 | assert.False(t, run.More(), "Fail after the timeout") 86 | } 87 | 88 | func TestUnlimitedStop(t *testing.T) { 89 | run := New(0 /* maxRequests */, 0 /* rps */, 0 /* maxDuration */) 90 | for i := 0; i < 5; i++ { 91 | assert.True(t, run.More(), "Unlimited should suceed till Stop") 92 | } 93 | run.Stop() 94 | time.Sleep(5 * time.Millisecond) 95 | assert.False(t, run.More(), "Fail after the timeout") 96 | } 97 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | // getLoggerVerbosity returns an appropriate logging level based on flags. 9 | func getLoggerVerbosity(verbosity []bool) zapcore.Level { 10 | switch len(verbosity) { 11 | case 0: 12 | return zap.WarnLevel 13 | case 1: 14 | return zap.InfoLevel 15 | default: 16 | return zap.DebugLevel 17 | } 18 | } 19 | 20 | func configureLoggerConfig(opts *Options) zap.Config { 21 | loggerConfig := zap.NewDevelopmentConfig() 22 | loggerConfig.EncoderConfig = zapcore.EncoderConfig{ 23 | // Keys can be anything except the empty string. 24 | TimeKey: "T", 25 | LevelKey: "L", 26 | NameKey: "N", 27 | CallerKey: "C", 28 | MessageKey: "M", 29 | StacktraceKey: "S", 30 | EncodeLevel: zapcore.CapitalLevelEncoder, 31 | EncodeTime: zapcore.ISO8601TimeEncoder, 32 | EncodeDuration: zapcore.StringDurationEncoder, 33 | } 34 | 35 | // Set the logger level based on the command line parsed options. 36 | loggerConfig.Level.SetLevel(getLoggerVerbosity(opts.Verbosity)) 37 | 38 | return loggerConfig 39 | } 40 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | func TestLoggerConfigure(t *testing.T) { 12 | tests := []struct { 13 | options *Options 14 | wantLoggerLevel zapcore.Level 15 | }{ 16 | { 17 | options: &Options{Verbosity: []bool{}}, 18 | wantLoggerLevel: zapcore.WarnLevel, 19 | }, 20 | { 21 | options: &Options{Verbosity: []bool{true}}, 22 | wantLoggerLevel: zapcore.InfoLevel, 23 | }, 24 | { 25 | options: &Options{Verbosity: []bool{true, true}}, 26 | wantLoggerLevel: zapcore.DebugLevel, 27 | }, 28 | { 29 | options: &Options{Verbosity: []bool{true, true, true}}, 30 | wantLoggerLevel: zapcore.DebugLevel, 31 | }, 32 | { 33 | options: &Options{Verbosity: []bool{false}}, 34 | wantLoggerLevel: zapcore.InfoLevel, 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(fmt.Sprintf("Test logger: %v", tt.wantLoggerLevel), func(t *testing.T) { 39 | lconf := configureLoggerConfig(tt.options) 40 | assert.Equal(t, lconf.Level.Level(), tt.wantLoggerLevel) 41 | }) 42 | } 43 | } 44 | 45 | func TestLoggerGetLoggerVerbosity(t *testing.T) { 46 | tests := []struct { 47 | verbosity []bool 48 | wantLoggerLevel zapcore.Level 49 | }{ 50 | { 51 | verbosity: nil, 52 | wantLoggerLevel: zapcore.WarnLevel, 53 | }, 54 | { 55 | verbosity: []bool{true}, 56 | wantLoggerLevel: zapcore.InfoLevel, 57 | }, 58 | { 59 | verbosity: []bool{true, true}, 60 | wantLoggerLevel: zapcore.DebugLevel, 61 | }, 62 | { 63 | verbosity: []bool{true, true, true}, 64 | wantLoggerLevel: zapcore.DebugLevel, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(fmt.Sprintf("getLoggerVerbosity(%v)", tt.verbosity), func(t *testing.T) { 69 | lvl := getLoggerVerbosity(tt.verbosity) 70 | assert.Equal(t, lvl, tt.wantLoggerLevel) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestTimeMillisFlag(t *testing.T) { 31 | tests := []struct { 32 | value string 33 | want time.Duration 34 | wantErr bool 35 | }{ 36 | { 37 | // values without a unit should be treated as milliseconds 38 | value: "100", 39 | want: 100 * time.Millisecond, 40 | }, 41 | { 42 | value: "0.1s", 43 | want: 100 * time.Millisecond, 44 | }, 45 | { 46 | value: "0.1", 47 | wantErr: true, 48 | }, 49 | { 50 | value: "notanumber", 51 | wantErr: true, 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | var timeMillis timeMillisFlag 57 | 58 | err := timeMillis.UnmarshalFlag(tt.value) 59 | if tt.wantErr { 60 | assert.Error(t, err, "UnmarshalFlag(%v) should fail", tt.value) 61 | continue 62 | } 63 | 64 | assert.NoError(t, err, "UnmarshalFlag(%v) should not fail", tt.value) 65 | assert.Equal(t, tt.want, timeMillis.Duration(), "UnmarshalFlag(%v) expected %v", tt.value, tt.want) 66 | assert.Equal(t, tt.want.String(), timeMillis.String(), "String mismatch") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | "log" 27 | "os" 28 | ) 29 | 30 | type output interface { 31 | io.Writer 32 | 33 | Fatalf(format string, args ...interface{}) 34 | Printf(format string, args ...interface{}) 35 | Warnf(format string, args ...interface{}) 36 | } 37 | 38 | type consoleOutput struct { 39 | *os.File 40 | } 41 | 42 | func (consoleOutput) Fatalf(format string, args ...interface{}) { 43 | log.Fatalf(format, args...) 44 | } 45 | 46 | func (consoleOutput) Printf(format string, args ...interface{}) { 47 | fmt.Printf(format, args...) 48 | } 49 | 50 | func (consoleOutput) Warnf(format string, args ...interface{}) { 51 | fmt.Fprintf(os.Stderr, format, args...) 52 | } 53 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | flags "github.com/jessevdk/go-flags" 5 | "github.com/yarpc/yab/plugin" 6 | ) 7 | 8 | // provides a bridge between the plugin::Parser interface and the go-flags::Parser struct 9 | type pluginParserAdapter struct { 10 | *flags.Parser 11 | } 12 | 13 | func (p pluginParserAdapter) AddFlagGroup(shortDescription, longDescription string, data interface{}) error { 14 | _, err := p.AddGroup(shortDescription, longDescription, data) 15 | return err 16 | } 17 | 18 | var _ plugin.Parser = (*pluginParserAdapter)(nil) 19 | -------------------------------------------------------------------------------- /peerprovider/file.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/url" 8 | ) 9 | 10 | type filePeerProvider struct{} 11 | 12 | func (filePeerProvider) Resolve(ctx context.Context, url *url.URL) ([]string, error) { 13 | return parsePeerList(url.Path) 14 | } 15 | 16 | func parsePeerList(filename string) ([]string, error) { 17 | contents, err := ioutil.ReadFile(filename) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to open peer list: %v", err) 20 | } 21 | 22 | return parsePeers(contents) 23 | } 24 | -------------------------------------------------------------------------------- /peerprovider/file_test.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParseHostFile(t *testing.T) { 10 | tests := []struct { 11 | filename string 12 | errMsg string 13 | want []string 14 | }{ 15 | { 16 | filename: "/fake/file", 17 | errMsg: "failed to open peer list", 18 | }, 19 | { 20 | filename: "valid_peerlist.json", 21 | want: []string{"1.1.1.1:1", "2.2.2.2:2"}, 22 | }, 23 | { 24 | filename: "valid_peerlist.yaml", 25 | want: []string{"1.1.1.1:1", "2.2.2.2:2"}, 26 | }, 27 | { 28 | filename: "valid_url_peerlist.yaml", 29 | want: []string{"http://1.1.1.1:1/foo", "tchannel://2.2.2.2:2"}, 30 | }, 31 | { 32 | filename: "valid_url_peerlist.txt", 33 | want: []string{"http://1.1.1.1:1/foo", "tchannel://2.2.2.2:2"}, 34 | }, 35 | { 36 | filename: "valid_peerlist.txt", 37 | want: []string{"1.1.1.1:1", "2.2.2.2:2"}, 38 | }, 39 | { 40 | filename: "invalid_peerlist.json", 41 | errMsg: errPeerListFile.Error(), 42 | }, 43 | { 44 | filename: "invalid.json", 45 | errMsg: errPeerListFile.Error(), 46 | }, 47 | { 48 | filename: "invalid_url_peerlist.txt", 49 | errMsg: errPeerListFile.Error(), 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | got, err := parsePeerList("../testdata/" + tt.filename) 55 | if tt.errMsg != "" { 56 | if assert.Error(t, err, "parsePeerList(%v) should fail", tt.filename) { 57 | assert.Contains(t, err.Error(), tt.errMsg, "Unexpected error for parsePeerList(%v)", tt.filename) 58 | } 59 | continue 60 | } 61 | 62 | if assert.NoError(t, err, "parsePeerList(%v) should not fail", tt.filename) { 63 | assert.Equal(t, tt.want, got, "parsePeerList(%v) mismatch", tt.filename) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /peerprovider/http.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type httpPeerProvider struct{} 12 | 13 | func (httpPeerProvider) Resolve(ctx context.Context, url *url.URL) ([]string, error) { 14 | req, err := http.NewRequest("GET", url.String(), nil) 15 | if err != nil { 16 | return nil, err 17 | } 18 | req = req.WithContext(ctx) 19 | 20 | resp, err := http.DefaultClient.Do(req) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to read peer list over HTTP: %v", err) 23 | } 24 | if resp.StatusCode != http.StatusOK { 25 | return nil, fmt.Errorf("failed to read peer list over HTTP, status not OK: %v", http.StatusText(resp.StatusCode)) 26 | } 27 | defer resp.Body.Close() 28 | 29 | contents, err := ioutil.ReadAll(resp.Body) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to read entire contents of HTTP body for peer list: %v", err) 32 | } 33 | 34 | return parsePeers(contents) 35 | } 36 | -------------------------------------------------------------------------------- /peerprovider/http_test.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestHTTPResolve(t *testing.T) { 15 | svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | io.WriteString(w, "- 1.1.1.1:1\n") 17 | io.WriteString(w, "- 2.2.2.2:2\n") 18 | })) 19 | defer svr.Close() 20 | 21 | pp := httpPeerProvider{} 22 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 23 | defer cancel() 24 | peers, err := pp.Resolve(ctx, mustParseURL(svr.URL)) 25 | assert.NoError(t, err, "error resolving peers") 26 | assert.Equal(t, peers, []string{ 27 | "1.1.1.1:1", 28 | "2.2.2.2:2", 29 | }, "received expected peers") 30 | } 31 | 32 | func TestHTTPResolveFails(t *testing.T) { 33 | svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | w.WriteHeader(http.StatusNotFound) 35 | })) 36 | defer svr.Close() 37 | 38 | pp := httpPeerProvider{} 39 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 40 | defer cancel() 41 | _, err := pp.Resolve(ctx, mustParseURL(svr.URL)) 42 | assert.Error(t, err, "error resolving peers") 43 | } 44 | 45 | func TestHTTPResolveNetworkError(t *testing.T) { 46 | pp := httpPeerProvider{} 47 | ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) 48 | defer cancel() 49 | _, err := pp.Resolve(ctx, mustParseURL("http://192.0.2.1:888/peers.txt")) 50 | assert.Error(t, err, "error resolving peers") 51 | } 52 | -------------------------------------------------------------------------------- /peerprovider/interface.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | var registry = make(map[string]PeerProvider) 10 | 11 | func init() { 12 | RegisterPeerProvider("", filePeerProvider{}) 13 | RegisterPeerProvider("file", filePeerProvider{}) 14 | RegisterPeerProvider("http", httpPeerProvider{}) 15 | RegisterPeerProvider("https", httpPeerProvider{}) 16 | } 17 | 18 | // Schemes returns supported peer provider protocol schemes. 19 | func Schemes() []string { 20 | schemes := make([]string, 0, len(registry)) 21 | for scheme := range registry { 22 | if scheme != "" { 23 | schemes = append(schemes, scheme) 24 | } 25 | } 26 | return schemes 27 | } 28 | 29 | // Resolve resolves a peer list from a URL, using the registered 30 | // peer provider for that protocol scheme, albeit "file", "http", etc. 31 | func Resolve(ctx context.Context, u *url.URL) ([]string, error) { 32 | if pp, ok := registry[u.Scheme]; ok { 33 | return pp.Resolve(ctx, u) 34 | } 35 | 36 | return nil, fmt.Errorf("no peer provider available for scheme %q in URL %q", u.Scheme, u.String()) 37 | } 38 | 39 | // PeerProvider provides a list of peers for a given peer provider URL. 40 | // Implementations are expected to define the behavior for the URL name space 41 | // and return strings suitable for passing to `--peer` for whatever protocol 42 | // the name specifies. 43 | type PeerProvider interface { 44 | Resolve(context.Context, *url.URL) ([]string, error) 45 | } 46 | 47 | // RegisterPeerProvider registers a peer provider for a resolver protocol 48 | func RegisterPeerProvider(scheme string, pp PeerProvider) { 49 | registry[scheme] = pp 50 | } 51 | -------------------------------------------------------------------------------- /peerprovider/interface_test.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type fakePeerProvider struct { 14 | res []string 15 | err error 16 | } 17 | 18 | func (fpp *fakePeerProvider) Resolve(ctx context.Context, u *url.URL) ([]string, error) { 19 | return fpp.res, fpp.err 20 | } 21 | 22 | func TestSchemes(t *testing.T) { 23 | defer stubRegistry()() 24 | 25 | f := &fakePeerProvider{res: []string{"who's on first"}} 26 | RegisterPeerProvider("fake", f) 27 | 28 | assert.Equal(t, registry, map[string]PeerProvider{ 29 | "fake": f, 30 | }, "Expected registered fake") 31 | 32 | res, err := Resolve(context.Background(), mustParseURL("fake://dekaf")) 33 | assert.NoError(t, err, "should resolve without error") 34 | assert.Equal(t, res, f.res, "should resolve fake:// name") 35 | } 36 | 37 | func TestResolveError(t *testing.T) { 38 | defer stubRegistry()() 39 | 40 | f := &fakePeerProvider{err: fmt.Errorf("noope")} 41 | RegisterPeerProvider("fake", f) 42 | 43 | res, err := Resolve(context.Background(), mustParseURL("fake://dekaf")) 44 | assert.Equal(t, err, f.err, "should resolve ith error") 45 | assert.Equal(t, res, []string(nil), "should not resolve") 46 | } 47 | 48 | func TestEmptyScheme(t *testing.T) { 49 | peers, err := Resolve(context.Background(), mustParseURL("../testdata/valid_peerlist.txt")) 50 | assert.NoError(t, err, "error attempting to resolve peer list file") 51 | assert.Equal(t, peers, []string{ 52 | "1.1.1.1:1", 53 | "2.2.2.2:2", 54 | }, "obtains expected peers") 55 | } 56 | 57 | func TestFileScheme(t *testing.T) { 58 | abs, _ := filepath.Abs("../testdata/valid_peerlist.yaml") 59 | u := mustParseURL("file:" + abs) 60 | peers, err := Resolve(context.Background(), u) 61 | assert.NoError(t, err, "error attempting to resolve peer list file") 62 | assert.Equal(t, peers, []string{ 63 | "1.1.1.1:1", 64 | "2.2.2.2:2", 65 | }, "obtains expected peers") 66 | } 67 | -------------------------------------------------------------------------------- /peerprovider/parse.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/url" 11 | "strings" 12 | 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | var errPeerListFile = errors.New("peer list should be YAML, JSON, or newline delimited strings") 17 | 18 | // parsePeers accepts a file in YAML, JSON, or newline-delimited format, 19 | // containing host:port peer addresses. 20 | func parsePeers(contents []byte) ([]string, error) { 21 | // Try as JSON. 22 | hosts, err := parseYAMLPeers(contents) 23 | if err != nil { 24 | hosts, err = parseNewlineDelimitedPeers(bytes.NewReader(contents)) 25 | } 26 | if err != nil { 27 | return nil, errPeerListFile 28 | } 29 | 30 | return hosts, nil 31 | } 32 | 33 | func parseYAMLPeers(contents []byte) ([]string, error) { 34 | var hosts []string 35 | return hosts, yaml.Unmarshal(contents, &hosts) 36 | } 37 | 38 | func parseNewlineDelimitedPeers(r io.Reader) ([]string, error) { 39 | var hosts []string 40 | rdr := bufio.NewReader(r) 41 | for { 42 | line, err := rdr.ReadString('\n') 43 | if line == "" && err != nil { 44 | if err == io.EOF { 45 | break 46 | } 47 | return nil, err 48 | } 49 | 50 | line = strings.TrimSpace(line) 51 | if line == "" { 52 | continue 53 | } 54 | 55 | // If the line is a host:port or a URL, then it's valid. 56 | _, _, hostPortErr := net.SplitHostPort(line) 57 | if hostPortErr == nil { 58 | hosts = append(hosts, line) 59 | continue 60 | } 61 | 62 | urlParseErr := isValidURL(line) 63 | if urlParseErr == nil { 64 | hosts = append(hosts, line) 65 | continue 66 | } 67 | 68 | return nil, fmt.Errorf("failed to parse line %q as host:port (%v) or URL (%v)", line, hostPortErr, urlParseErr) 69 | } 70 | 71 | return hosts, nil 72 | } 73 | 74 | func isValidURL(line string) error { 75 | u, err := url.Parse(line) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if u.Host == "" { 81 | return fmt.Errorf("url cannot have empty host: %v", line) 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /peerprovider/util_for_test.go: -------------------------------------------------------------------------------- 1 | package peerprovider 2 | 3 | import "net/url" 4 | 5 | func mustParseURL(s string) *url.URL { 6 | u, _ := url.Parse(s) 7 | return u 8 | } 9 | 10 | func stubRegistry() func() { 11 | oldRegistry := registry 12 | registry = make(map[string]PeerProvider) 13 | return func() { registry = oldRegistry } 14 | } 15 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.uber.org/multierr" 7 | ) 8 | 9 | type flag struct { 10 | groupName string 11 | longDescription string 12 | data interface{} 13 | } 14 | 15 | // stores the list of currently registered flags 16 | var _flags []*flag 17 | 18 | // AddFlags should be used by embedded modules to inject custom flags into `yab`. 19 | // It adds a set of custom flags with heading `groupName`. 20 | // 21 | // The `data` argument should be a pointer to a struct with one field for each 22 | // command line flag. Each field should use tags like `description` to inform 23 | // the parser of metadata about the flag. (tags are the same as supported by 24 | // github.com/jessevdk/go-flags) 25 | // 26 | // type foo struct { 27 | // Bar string `long:"bar" description:"Sets the 'bar' value."` 28 | // } 29 | // 30 | // AddFlags("Foo Options", "", &foo{}) 31 | // 32 | // This would inject a custom flag group that looks like: 33 | // 34 | // Foo Options 35 | // --bar Sets the 'bar' value. 36 | // 37 | // In order to retrieve the mutated results of the set flag, users of AddFlags() should 38 | // retain a reference to the `data` object and check its values after parsing is complete. 39 | func AddFlags(groupName string, longDescription string, data interface{}) { 40 | _flags = append(_flags, &flag{ 41 | groupName: groupName, 42 | longDescription: longDescription, 43 | data: data, 44 | }) 45 | } 46 | 47 | // Parser is any object that can add flag groups to itself before performing its parse. 48 | type Parser interface { 49 | // AddFlagGroup adds an additional flag group to process during parsing. 50 | AddFlagGroup(groupName, longDescription string, data interface{}) error 51 | } 52 | 53 | // AddToParser adds all registered flags to the passed Parser. 54 | // This operation is not atomic, flags are applied on a best-effort basis (not "all-or-nothing") 55 | // Returns a slice of errors indicating which flags groups failed to be added. 56 | func AddToParser(p Parser) error { 57 | var err error 58 | for _, f := range _flags { 59 | flagErr := p.AddFlagGroup(f.groupName, f.longDescription, f.data) 60 | if flagErr != nil { 61 | err = multierr.Append(err, fmt.Errorf("adding %v to parser: %v", f.groupName, flagErr)) 62 | } 63 | } 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "go.uber.org/multierr" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type mockParser struct { 14 | flags []*flag 15 | shouldErr bool 16 | errOnCallCount int 17 | addFlagGroupCallCount int 18 | } 19 | 20 | func (p *mockParser) AddFlagGroup(short, long string, data interface{}) error { 21 | if p.shouldErr && p.errOnCallCount == p.addFlagGroupCallCount { 22 | return errors.New("bad") 23 | } 24 | p.flags = append(p.flags, &flag{ 25 | groupName: short, 26 | longDescription: long, 27 | data: data, 28 | }) 29 | p.addFlagGroupCallCount++ 30 | return nil 31 | } 32 | 33 | type fooFlags struct { 34 | Bar string `long:"bar" description:"Sets the 'bar' value."` 35 | } 36 | 37 | // defer-able test cleanup for global state 38 | func setFlags(flags []*flag) (restore func()) { 39 | _flags = flags 40 | return func() { 41 | _flags = nil 42 | } 43 | } 44 | 45 | func TestAddToParserSuccess(t *testing.T) { 46 | flags := []*flag{{}} 47 | defer setFlags(flags)() 48 | p := &mockParser{} 49 | 50 | err := AddToParser(p) 51 | assert.NoError(t, err) 52 | assert.Equal(t, 1, p.addFlagGroupCallCount) 53 | assert.Equal(t, 1, len(p.flags)) 54 | } 55 | 56 | func TestAddToParserFail(t *testing.T) { 57 | flags := []*flag{{}} 58 | defer setFlags(flags)() 59 | p := &mockParser{ 60 | shouldErr: true, 61 | } 62 | 63 | err := AddToParser(p) 64 | assert.Error(t, err) 65 | errs := multierr.Errors(err) 66 | assert.Equal(t, 1, len(errs)) 67 | assert.Equal(t, 0, p.addFlagGroupCallCount) 68 | assert.Equal(t, 0, len(p.flags)) 69 | } 70 | 71 | func TestAddToParserFailPartial(t *testing.T) { 72 | flags := []*flag{{}, {}, {}} 73 | defer setFlags(flags)() 74 | p := &mockParser{ 75 | shouldErr: true, 76 | errOnCallCount: 2, 77 | } 78 | 79 | err := AddToParser(p) 80 | assert.Error(t, err) 81 | errs := multierr.Errors(err) 82 | assert.Equal(t, 1, len(errs)) 83 | assert.Equal(t, 2, p.addFlagGroupCallCount) 84 | assert.Equal(t, 2, len(p.flags)) 85 | } 86 | 87 | func TestAddToParserMany(t *testing.T) { 88 | numFlags := 100 89 | mockFlags := make([]*flag, numFlags) 90 | for i := range mockFlags { 91 | mockFlags[i] = &flag{groupName: fmt.Sprintf("foo-%d", i)} 92 | } 93 | defer setFlags(mockFlags)() 94 | 95 | p := &mockParser{} 96 | 97 | errs := AddToParser(p) 98 | assert.NoError(t, errs) 99 | assert.Equal(t, numFlags, p.addFlagGroupCallCount) 100 | assert.Equal(t, numFlags, len(p.flags)) 101 | 102 | for i := range mockFlags { 103 | assert.Equal(t, mockFlags[i], p.flags[i]) 104 | } 105 | } 106 | 107 | func TestAddFlags(t *testing.T) { 108 | defer setFlags(nil)() 109 | short := "Foo Options" 110 | long := "This is a lot of usage information about Foo" 111 | foo := &fooFlags{} 112 | 113 | AddFlags(short, long, foo) 114 | assert.Equal(t, 1, len(_flags)) 115 | 116 | setFlag := _flags[0] 117 | assert.Equal(t, short, setFlag.groupName) 118 | assert.Equal(t, long, setFlag.longDescription) 119 | assert.Equal(t, foo, setFlag.data) 120 | } 121 | 122 | func TestAddedFlagsArePassedToParser(t *testing.T) { 123 | defer setFlags(nil)() 124 | short := "Foo Options" 125 | long := "This is a lot of usage information about Foo" 126 | foo := &fooFlags{} 127 | 128 | p := &mockParser{} 129 | AddFlags(short, long, foo) 130 | errs := AddToParser(p) 131 | assert.NoError(t, errs) 132 | 133 | assert.Equal(t, 1, p.addFlagGroupCallCount) 134 | assert.Equal(t, 1, len(p.flags)) 135 | 136 | setFlag := p.flags[0] 137 | assert.Equal(t, short, setFlag.groupName) 138 | assert.Equal(t, long, setFlag.longDescription) 139 | assert.Equal(t, foo, setFlag.data) 140 | } 141 | -------------------------------------------------------------------------------- /protobuf/filedescriptorsets.go: -------------------------------------------------------------------------------- 1 | package protobuf 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/golang/protobuf/proto" 8 | "github.com/golang/protobuf/protoc-gen-go/descriptor" 9 | "github.com/jhump/protoreflect/desc" 10 | "github.com/yarpc/yab/encoding/encodingerror" 11 | ) 12 | 13 | // NewDescriptorProviderFileDescriptorSetBins creates a DescriptorSource that is backed by the named files, whose contents 14 | // are encoded FileDescriptorSet protos. 15 | func NewDescriptorProviderFileDescriptorSetBins(fileNames ...string) (DescriptorProvider, error) { 16 | files := &descriptor.FileDescriptorSet{} 17 | for _, fileName := range fileNames { 18 | b, err := ioutil.ReadFile(fileName) 19 | if err != nil { 20 | return nil, fmt.Errorf("could not load protoset file %q: %v", fileName, err) 21 | } 22 | var fs descriptor.FileDescriptorSet 23 | err = proto.Unmarshal(b, &fs) 24 | if err != nil { 25 | return nil, fmt.Errorf("could not parse contents of protoset file %q: %v", fileName, err) 26 | } 27 | files.File = append(files.File, fs.File...) 28 | } 29 | return NewDescriptorProviderFileDescriptorSet(files) 30 | } 31 | 32 | // NewDescriptorProviderFileDescriptorSet creates a DescriptorSource that is backed by the FileDescriptorSet. 33 | func NewDescriptorProviderFileDescriptorSet(files *descriptor.FileDescriptorSet) (DescriptorProvider, error) { 34 | unresolved := make(map[string]*descriptor.FileDescriptorProto, len(files.File)) 35 | for _, fd := range files.File { 36 | unresolved[fd.GetName()] = fd 37 | } 38 | resolved := map[string]*desc.FileDescriptor{} 39 | for _, fd := range files.File { 40 | if _, err := resolveFileDescriptor(unresolved, resolved, fd.GetName()); err != nil { 41 | return nil, err 42 | } 43 | } 44 | return &fileSource{files: resolved}, nil 45 | } 46 | 47 | func resolveFileDescriptor(unresolved map[string]*descriptor.FileDescriptorProto, resolved map[string]*desc.FileDescriptor, filename string) (*desc.FileDescriptor, error) { 48 | if r, ok := resolved[filename]; ok { 49 | return r, nil 50 | } 51 | fd, ok := unresolved[filename] 52 | if !ok { 53 | return nil, fmt.Errorf("no descriptor found for %q", filename) 54 | } 55 | deps := make([]*desc.FileDescriptor, 0, len(fd.GetDependency())) 56 | for _, dep := range fd.GetDependency() { 57 | depFd, err := resolveFileDescriptor(unresolved, resolved, dep) 58 | if err != nil { 59 | return nil, err 60 | } 61 | deps = append(deps, depFd) 62 | } 63 | result, err := desc.CreateFileDescriptor(fd, deps...) 64 | if err != nil { 65 | return nil, err 66 | } 67 | resolved[filename] = result 68 | return result, nil 69 | } 70 | 71 | type fileSource struct { 72 | files map[string]*desc.FileDescriptor 73 | } 74 | 75 | func (fs *fileSource) FindService(fullyQualifiedName string) (*desc.ServiceDescriptor, error) { 76 | var available []string 77 | 78 | for _, fd := range fs.files { 79 | for _, svc := range fd.GetServices() { 80 | if svc.GetFullyQualifiedName() == fullyQualifiedName { 81 | return svc, nil 82 | } 83 | 84 | available = append(available, svc.GetFullyQualifiedName()) 85 | } 86 | } 87 | 88 | return nil, encodingerror.NotFound{ 89 | Encoding: "gRPC", 90 | SearchType: "service", 91 | Search: fullyQualifiedName, 92 | Available: available, 93 | } 94 | } 95 | 96 | func (fs *fileSource) FindMessage(messageType string) (*desc.MessageDescriptor, error) { 97 | for _, fd := range fs.files { 98 | if md := fd.FindMessage(messageType); md != nil { 99 | return md, nil 100 | } 101 | } 102 | return nil, nil 103 | } 104 | 105 | func (fs *fileSource) Close() {} 106 | -------------------------------------------------------------------------------- /protobuf/filedescriptorsets_test.go: -------------------------------------------------------------------------------- 1 | package protobuf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNewDescriptorProviderFileDescriptorSetBins(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | fileNames []string 14 | errMsg string 15 | lookupSymbol string 16 | symbolErrMsg string 17 | }{ 18 | { 19 | name: "pass", 20 | fileNames: []string{"../testdata/protobuf/simple/simple.proto.bin"}, 21 | lookupSymbol: "Bar", 22 | }, 23 | { 24 | name: "pass combined dependencies", 25 | fileNames: []string{"../testdata/protobuf/dependencies/combined.bin"}, 26 | lookupSymbol: "Bar", 27 | }, 28 | { 29 | name: "pass combined dependencies (multiroot)", 30 | fileNames: []string{"../testdata/protobuf/multiroot/root/combined.bin"}, 31 | lookupSymbol: "Bar", 32 | }, 33 | { 34 | name: "pass combined dependencies (nested)", 35 | fileNames: []string{"../testdata/protobuf/nested/combined.bin"}, 36 | lookupSymbol: "Bar", 37 | }, 38 | { 39 | name: "pass multiple dependencies", 40 | fileNames: []string{ 41 | "../testdata/protobuf/dependencies/main.proto.bin", 42 | "../testdata/protobuf/dependencies/dep.proto.bin", 43 | }, 44 | lookupSymbol: "Bar", 45 | }, 46 | { 47 | name: "pass parsing fail finding symbol", 48 | fileNames: []string{"../testdata/protobuf/simple/simple.proto.bin"}, 49 | lookupSymbol: "Bar.Baq", 50 | symbolErrMsg: `could not find gRPC service "Bar.Baq"`, 51 | }, 52 | { 53 | name: "fail is not protoset", 54 | fileNames: []string{"../testdata/protobuf/simple/simple.proto"}, 55 | errMsg: `could not parse contents of protoset file`, 56 | }, 57 | { 58 | name: "fail doesn't exist", 59 | fileNames: []string{"../testdata/protobuf/simple/not_existing_simple.proto"}, 60 | errMsg: `simple.proto: no such file or directory`, 61 | }, 62 | { 63 | name: "fail missing dependencies", 64 | fileNames: []string{"../testdata/protobuf/dependencies/main.proto.bin"}, 65 | errMsg: `no descriptor found for "dep.proto"`, 66 | }, 67 | { 68 | name: "fail incomplete", 69 | fileNames: []string{ 70 | "../testdata/protobuf/dependencies/main.proto.bin", 71 | "../testdata/protobuf/dependencies/other.bin", 72 | }, 73 | errMsg: `included an unresolvable reference`, 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | got, err := NewDescriptorProviderFileDescriptorSetBins(tt.fileNames...) 79 | if tt.errMsg != "" { 80 | require.Error(t, err) 81 | assert.Contains(t, err.Error(), tt.errMsg, "%v: invalid error", tt.name) 82 | return 83 | } 84 | require.NoError(t, err) 85 | require.NotNil(t, got) 86 | 87 | // Doesn't do anything, but is part of the API. 88 | defer got.Close() 89 | 90 | if tt.lookupSymbol != "" { 91 | require.NotNil(t, got) 92 | s, err := got.FindService(tt.lookupSymbol) 93 | if tt.symbolErrMsg != "" { 94 | require.Error(t, err) 95 | assert.Contains(t, err.Error(), tt.symbolErrMsg, "%v: invalid error", tt.name) 96 | return 97 | } 98 | require.NoError(t, err) 99 | assert.Equal(t, tt.lookupSymbol, s.GetFullyQualifiedName()) 100 | } 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /protobuf/source.go: -------------------------------------------------------------------------------- 1 | package protobuf 2 | 3 | import ( 4 | "github.com/jhump/protoreflect/desc" 5 | ) 6 | 7 | // DescriptorProvider is a source of protobuf descriptor information. It can be backed by a FileDescriptorSet 8 | // proto (like a file generated by protoc). 9 | type DescriptorProvider interface { 10 | // FindService returns a service descriptor for the given fully-qualified symbol name. 11 | FindService(fullyQualifiedName string) (*desc.ServiceDescriptor, error) 12 | 13 | // FindMessage return a message descriptor for the given fully-qualified symbol name. 14 | FindMessage(messageType string) (*desc.MessageDescriptor, error) 15 | 16 | Close() 17 | } 18 | -------------------------------------------------------------------------------- /protobuf/source_reflection.go: -------------------------------------------------------------------------------- 1 | package protobuf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jhump/protoreflect/desc" 11 | "github.com/jhump/protoreflect/grpcreflect" 12 | "github.com/yarpc/yab/encoding/encodingerror" 13 | yproto "go.uber.org/yarpc/encoding/protobuf" 14 | ygrpc "go.uber.org/yarpc/transport/grpc" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/metadata" 17 | rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" 18 | "google.golang.org/grpc/resolver" 19 | "google.golang.org/grpc/resolver/manual" 20 | ) 21 | 22 | // ReflectionArgs are args for constructing a DescriptorProvider that reaches out to a reflection server. 23 | type ReflectionArgs struct { 24 | Caller string 25 | Service string 26 | RoutingDelegate string 27 | RoutingKey string 28 | Peers []string 29 | Timeout time.Duration 30 | } 31 | 32 | // NewDescriptorProviderReflection returns a DescriptorProvider that reaches 33 | // out to a reflection server to access file descriptors. 34 | func NewDescriptorProviderReflection(args ReflectionArgs) (DescriptorProvider, error) { 35 | r, deregisterScheme := GenerateAndRegisterManualResolver() 36 | defer deregisterScheme() 37 | peers := make([]resolver.Address, len(args.Peers)) 38 | for i, p := range args.Peers { 39 | if strings.Contains(p, "://") { 40 | return nil, fmt.Errorf("peer contains scheme %q", p) 41 | } 42 | peers[i] = resolver.Address{Addr: p, Type: resolver.Backend} 43 | } 44 | r.InitialState(resolver.State{Addresses: peers}) 45 | 46 | ctx, cancel := context.WithTimeout(context.Background(), args.Timeout) 47 | conn, err := grpc.DialContext( 48 | ctx, 49 | r.Scheme()+":///", // minimal target to dial registered host:port pairs 50 | grpc.WithAuthority(args.Service), 51 | grpc.WithBlock(), 52 | grpc.WithInsecure()) 53 | if err != nil { 54 | cancel() 55 | return nil, fmt.Errorf("could not reach reflection server: %s", err) 56 | } 57 | pbClient := rpb.NewServerReflectionClient(conn) 58 | 59 | routingHeaders := metadata.Pairs( 60 | ygrpc.CallerHeader, args.Caller, 61 | ygrpc.ServiceHeader, args.Service, 62 | ygrpc.EncodingHeader, string(yproto.Encoding), 63 | ) 64 | if args.RoutingDelegate != "" { 65 | routingHeaders.Append(ygrpc.RoutingDelegateHeader, args.RoutingDelegate) 66 | } 67 | if args.RoutingKey != "" { 68 | routingHeaders.Append(ygrpc.RoutingKeyHeader, args.RoutingKey) 69 | } 70 | 71 | metadataContext := metadata.NewOutgoingContext(ctx, routingHeaders) 72 | return &grpcreflectSource{ 73 | client: grpcreflect.NewClient(metadataContext, pbClient), 74 | cancelFunc: cancel, 75 | }, nil 76 | } 77 | 78 | type grpcreflectSource struct { 79 | client *grpcreflect.Client 80 | cancelFunc context.CancelFunc 81 | } 82 | 83 | func (s *grpcreflectSource) FindMessage(messageType string) (*desc.MessageDescriptor, error) { 84 | msg, err := s.client.ResolveMessage(messageType) 85 | 86 | if grpcreflect.IsElementNotFoundError(err) { 87 | // If we couldn't find the message through the client, 88 | // return nil instead to follow the contract 89 | return nil, nil 90 | } 91 | 92 | if err != nil { 93 | return nil, wrapReflectionError(err) 94 | } 95 | 96 | return msg, err 97 | } 98 | 99 | func (s *grpcreflectSource) FindService(fullyQualifiedName string) (*desc.ServiceDescriptor, error) { 100 | service, err := s.client.ResolveService(fullyQualifiedName) 101 | if err != nil { 102 | if !grpcreflect.IsElementNotFoundError(err) { 103 | return nil, wrapReflectionError(err) 104 | } 105 | 106 | available, availableErr := s.client.ListServices() 107 | if availableErr != nil && !grpcreflect.IsElementNotFoundError(availableErr) { 108 | return nil, wrapReflectionError(availableErr) 109 | } 110 | 111 | return nil, encodingerror.NotFound{ 112 | Encoding: "gRPC", 113 | SearchType: "service", 114 | Search: fullyQualifiedName, 115 | Example: "--method Service/Method", 116 | Available: available, 117 | } 118 | } 119 | 120 | return service, nil 121 | } 122 | 123 | func (s *grpcreflectSource) Close() { 124 | s.cancelFunc() 125 | s.client.Reset() 126 | } 127 | 128 | func wrapReflectionError(err error) error { 129 | return fmt.Errorf("error in protobuf reflection: %v", err) 130 | } 131 | 132 | func GenerateAndRegisterManualResolver() (*manual.Resolver, func()) { 133 | scheme := strconv.FormatInt(time.Now().UnixNano(), 36) 134 | r := manual.NewBuilderWithScheme(scheme) 135 | resolver.Register(r) 136 | return r, func() { resolver.UnregisterForTesting(scheme) } 137 | } 138 | -------------------------------------------------------------------------------- /raffle.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarpc/yab/c16a5d2148de2894ad0325cf7cfb5f738927c710/raffle.bin -------------------------------------------------------------------------------- /ratelimit/time_period.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ratelimit 22 | 23 | import ( 24 | "math" 25 | "sync" 26 | "time" 27 | ) 28 | 29 | // Note: This file is inspired by: 30 | // https://github.com/prashantv/go-bench/blob/master/ratelimit 31 | 32 | // Limiter is used to rate limit some process, possibly across goroutines. 33 | // The process is expected to call Take() before every iteration, which 34 | // may block to throttle the process. 35 | type Limiter interface { 36 | // Take should block to make sure that the RPS is met before returning true. 37 | // If the passed in channel is closed, Take should unblock and return false. 38 | Take(cancel <-chan struct{}) bool 39 | } 40 | 41 | type timePeriod struct { 42 | sync.Mutex 43 | timer *time.Timer 44 | last time.Time 45 | sleepFor time.Duration 46 | perRequest time.Duration 47 | maxSlack time.Duration 48 | } 49 | 50 | // New returns a Limiter that will limit to the given RPS. 51 | func New(rps int) Limiter { 52 | return &timePeriod{ 53 | perRequest: time.Second / time.Duration(rps), 54 | maxSlack: -10 * time.Second / time.Duration(rps), 55 | timer: time.NewTimer(time.Duration(math.MaxInt64)), 56 | } 57 | } 58 | 59 | // Take blocks to ensure that the time spent between multiple 60 | // Take calls is on average time.Second/rps. 61 | func (t *timePeriod) Take(cancel <-chan struct{}) bool { 62 | t.Lock() 63 | defer t.Unlock() 64 | 65 | // If this is our first request, then we allow it. 66 | cur := time.Now() 67 | if t.last.IsZero() { 68 | t.last = cur 69 | return true 70 | } 71 | 72 | // sleepFor calculates how much time we should sleep based on 73 | // the perRequest budget and how long the last request took. 74 | // Since the request may take longer than the budget, this number 75 | // can get negative, and is summed across requests. 76 | t.sleepFor += t.perRequest - cur.Sub(t.last) 77 | t.last = cur 78 | 79 | // We shouldn't allow sleepFor to get too negative, since it would mean that 80 | // a service that slowed down a lot for a short period of time would get 81 | // a much higher RPS following that. 82 | if t.sleepFor < t.maxSlack { 83 | t.sleepFor = t.maxSlack 84 | } 85 | 86 | // If sleepFor is positive, then we should sleep now. 87 | if t.sleepFor > 0 { 88 | t.timer.Reset(t.sleepFor) 89 | select { 90 | case <-t.timer.C: 91 | case <-cancel: 92 | return false 93 | } 94 | 95 | t.last = cur.Add(t.sleepFor) 96 | t.sleepFor = 0 97 | } 98 | 99 | return true 100 | } 101 | 102 | type dummy struct{} 103 | 104 | // NewInfinite returns a RateLimiter that is not limited. 105 | func NewInfinite() Limiter { 106 | return dummy{} 107 | } 108 | 109 | func (dummy) Take(_ <-chan struct{}) bool { return true } 110 | -------------------------------------------------------------------------------- /scripts/cross_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | VERSION="$1" 4 | if [ -z "$VERSION" ]; then 5 | echo "USAGE: $0 VERSION" 6 | exit 1 7 | fi 8 | 9 | OSs=(linux darwin) 10 | ARCHs=(amd64 arm64) 11 | 12 | for OS in "${OSs[@]}"; do 13 | for ARCH in "${ARCHs[@]}"; do 14 | echo Building "$OS/$ARCH" 15 | mkdir -p "build/$OS-$ARCH" . 16 | GOOS=$OS GOARCH=$ARCH go build -o "build/$OS-$ARCH/yab" . 17 | (cd "build/$OS-$ARCH" && zip "../yab-$VERSION-$OS-$ARCH.zip" yab) 18 | done 19 | done 20 | -------------------------------------------------------------------------------- /scripts/extract_changelog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) != 2 { 14 | fmt.Printf("USAGE: %v VERSION\n", os.Args[0]) 15 | os.Exit(1) 16 | } 17 | 18 | version := os.Args[1] 19 | if err := run(version, os.Stdout); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | 24 | const ( 25 | searching = iota 26 | foundHeader 27 | printing 28 | ) 29 | 30 | func run(version string, out io.Writer) error { 31 | if version[0] == 'v' { 32 | version = version[1:] 33 | } 34 | 35 | file, err := os.OpenFile("CHANGELOG.md", os.O_RDONLY, 0444) 36 | if err != nil { 37 | return err 38 | } 39 | defer file.Close() 40 | 41 | state := searching 42 | scanner := bufio.NewScanner(file) 43 | for scanner.Scan() { 44 | line := scanner.Text() 45 | 46 | switch state { 47 | case searching: 48 | if strings.HasPrefix(line, "# "+version+" (") { 49 | state = foundHeader 50 | } 51 | case foundHeader: 52 | if line == "" { 53 | continue 54 | } 55 | state = printing 56 | fallthrough 57 | case printing: 58 | if strings.HasPrefix(line, "# ") { 59 | // next version section 60 | return nil 61 | } 62 | fmt.Fprintln(out, line) 63 | default: 64 | return fmt.Errorf("unexpected state %v on line %q", state, line) 65 | } 66 | 67 | } 68 | 69 | if err := scanner.Err(); err != nil { 70 | return err 71 | } 72 | 73 | if state < printing { 74 | return fmt.Errorf("could not find version %q in changelog", version) 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail -o errtrace -o functrace 3 | IFS=$'\n\t' 4 | 5 | die() { 6 | echo "$@" >&2 7 | exit 1 8 | } 9 | 10 | if [ -z "${GITHUB_REPO:-}" ]; then 11 | die "GITHUB_REPO is not set." 12 | fi 13 | 14 | if [ -z "${GITHUB_TOKEN:-}" ]; then 15 | die "GITHUB_TOKEN is not set." 16 | fi 17 | 18 | VERSION="${1:-}" 19 | if [ -z "$VERSION" ]; then 20 | die "USAGE: $0 VERSION" 21 | fi 22 | VERSION="${VERSION#refs/tags/}" 23 | 24 | # Make it easier to debug failures 25 | failure() { 26 | local lineno=$1 27 | local msg=$2 28 | echo "Failed at $lineno: $msg" 29 | } 30 | trap 'failure ${LINENO} "$BASH_COMMAND"' ERR 31 | 32 | 33 | ./scripts/cross_build.sh "$VERSION" 34 | 35 | CHANGELOG=$(go run scripts/extract_changelog.go "$VERSION") 36 | 37 | echo "Releasing $VERSION" 38 | echo "" 39 | echo "CHANGELOG:" 40 | echo "$CHANGELOG" 41 | echo "" 42 | 43 | RELEASE_ARGS=$(echo '{}' | \ 44 | jq -Mr \ 45 | --arg version "$VERSION" \ 46 | --arg changelog "$CHANGELOG" \ 47 | '. + { 48 | tag_name: $version, 49 | name: $version, 50 | body: $changelog, 51 | }') 52 | 53 | RELEASE_URL="https://api.github.com/repos/$GITHUB_REPO/releases" 54 | RELEASE_OUT=$(echo "$RELEASE_ARGS" | curl --header "authorization: Bearer $GITHUB_TOKEN" -X POST --data @- "$RELEASE_URL") 55 | 56 | echo "Release to $RELEASE_URL got:" 57 | echo " $RELEASE_OUT" 58 | 59 | UPLOADS_URL=$(echo "$RELEASE_OUT" | \ 60 | jq -e -r '.upload_url' | \ 61 | 62 | # The UPLOADS_URL has a strange {?name,label} as a suffix that we need to remove. 63 | # E.g., https://uploads.github.com/repos/yarpc/yab/releases/11488347/assets{?name,label} 64 | cut -d '{' -f1) 65 | 66 | # Upon creating a release, we get back the URL to which assets should be 67 | # uploaded under .uploads_url. 68 | # 69 | # See https://developer.github.com/v3/repos/releases/#create-a-release for 70 | # more information. 71 | 72 | for FILE in build/yab-"$VERSION"-*.zip; do 73 | echo "Uploading $FILE" 74 | 75 | # See https://developer.github.com/v3/repos/releases/#upload-a-release-asset 76 | UPLOAD_URL="${UPLOADS_URL}?name=$(basename "$FILE")" 77 | curl --header "authorization: Bearer $GITHUB_TOKEN" -X POST \ 78 | --data-binary @"$FILE" -H 'Content-Type: application/zip' \ 79 | "$UPLOAD_URL" 80 | done 81 | -------------------------------------------------------------------------------- /scripts/updateLicense.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, print_function, division, unicode_literals 3 | ) 4 | 5 | import re 6 | import sys 7 | from datetime import datetime 8 | 9 | CURRENT_YEAR = datetime.today().year 10 | 11 | LICENSE_BLOB = """Copyright (c) %d Uber Technologies, Inc. 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE.""" % CURRENT_YEAR 30 | 31 | LICENSE_BLOB_LINES_GO = [ 32 | ('// ' + l).strip() + '\n' for l in LICENSE_BLOB.split('\n') 33 | ] 34 | 35 | COPYRIGHT_RE = re.compile(r'Copyright \(c\) (\d+)', re.I) 36 | 37 | 38 | def update_go_license(name): 39 | with open(name) as f: 40 | orig_lines = list(f) 41 | lines = list(orig_lines) 42 | 43 | found = False 44 | changed = False 45 | for i, line in enumerate(lines[:5]): 46 | m = COPYRIGHT_RE.search(line) 47 | if not m: 48 | continue 49 | 50 | found = True 51 | year = int(m.group(1)) 52 | if year == CURRENT_YEAR: 53 | break 54 | 55 | new_line = COPYRIGHT_RE.sub('Copyright (c) %d' % CURRENT_YEAR, line) 56 | assert line != new_line, ('Could not change year in: %s' % line) 57 | lines[i] = new_line 58 | changed = True 59 | break 60 | 61 | if not found: 62 | if 'Code generated by' in lines[0]: 63 | lines[1:1] = ['\n'] + LICENSE_BLOB_LINES_GO 64 | else: 65 | lines[0:0] = LICENSE_BLOB_LINES_GO + ['\n'] 66 | changed = True 67 | 68 | if changed: 69 | with open(name, 'w') as f: 70 | for line in lines: 71 | f.write(line) 72 | 73 | 74 | def main(): 75 | if len(sys.argv) == 1: 76 | print('USAGE: %s FILE ...' % sys.argv[0]) 77 | sys.exit(1) 78 | 79 | for name in sys.argv[1:]: 80 | if name.endswith('.go'): 81 | update_go_license(name) 82 | else: 83 | raise NotImplementedError('Unsupported file type: %s' % name) 84 | 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /scripts/updateLicenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | python scripts/updateLicense.py $(go list -json $(glide nv) | jq -r '.Dir + "/" + (.GoFiles | .[])') 7 | -------------------------------------------------------------------------------- /server_util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "errors" 25 | "testing" 26 | 27 | "github.com/opentracing/opentracing-go" 28 | "github.com/uber/jaeger-client-go" 29 | "github.com/uber/tchannel-go" 30 | "github.com/uber/tchannel-go/raw" 31 | "github.com/uber/tchannel-go/testutils" 32 | "go.uber.org/atomic" 33 | "golang.org/x/net/context" 34 | ) 35 | 36 | type server struct { 37 | ch *tchannel.Channel 38 | } 39 | 40 | type handler func(ctx context.Context, args *raw.Args) (*raw.Res, error) 41 | 42 | func withTracer(tracer opentracing.Tracer) func(*testutils.ChannelOpts) { 43 | return func(opts *testutils.ChannelOpts) { 44 | opts.Tracer = tracer 45 | } 46 | } 47 | 48 | func newServer(t *testing.T, options ...func(*testutils.ChannelOpts)) *server { 49 | opts := testutils.NewOpts().SetServiceName("foo").DisableLogVerification() 50 | for _, option := range options { 51 | option(opts) 52 | } 53 | ch := testutils.NewServer(t, opts) 54 | return &server{ 55 | ch: ch, 56 | } 57 | } 58 | 59 | func (s *server) register(name string, f handler) { 60 | testutils.RegisterFunc(s.ch, name, f) 61 | } 62 | 63 | func (s *server) transportOpts() TransportOptions { 64 | return TransportOptions{ 65 | CallerName: "bar", 66 | ServiceName: "foo", 67 | Peers: []string{s.hostPort()}, 68 | } 69 | } 70 | 71 | func (s *server) hostPort() string { 72 | return s.ch.PeerInfo().HostPort 73 | } 74 | 75 | func (s *server) shutdown() { 76 | s.ch.Close() 77 | } 78 | 79 | var methods = methodsT{} 80 | 81 | type methodsT struct{} 82 | 83 | func (methodsT) echo() handler { 84 | return func(ctx context.Context, args *raw.Args) (*raw.Res, error) { 85 | return &raw.Res{ 86 | Arg2: args.Arg2, 87 | Arg3: args.Arg3, 88 | }, nil 89 | } 90 | } 91 | 92 | func (methodsT) traceEnabled() handler { 93 | return func(ctx context.Context, args *raw.Args) (*raw.Res, error) { 94 | ret := byte(0x00) 95 | 96 | span := opentracing.SpanFromContext(ctx) 97 | if span != nil { 98 | if spanCtx, ok := span.Context().(jaeger.SpanContext); ok { 99 | if spanCtx.IsDebug() { 100 | ret = 0x01 101 | } 102 | } 103 | } 104 | 105 | return &raw.Res{ 106 | Arg3: []byte{ret}, 107 | }, nil 108 | } 109 | } 110 | 111 | func (methodsT) customArg3(arg3 []byte) handler { 112 | return func(ctx context.Context, args *raw.Args) (*raw.Res, error) { 113 | return &raw.Res{ 114 | Arg2: args.Arg2, 115 | Arg3: arg3, 116 | }, nil 117 | } 118 | } 119 | 120 | func (methodsT) errorIf(f func() bool) handler { 121 | return func(ctx context.Context, args *raw.Args) (*raw.Res, error) { 122 | if f() { 123 | return nil, errors.New("error") 124 | } 125 | 126 | return &raw.Res{ 127 | Arg2: args.Arg2, 128 | Arg3: args.Arg3, 129 | }, nil 130 | } 131 | } 132 | 133 | func (methodsT) counter() (*atomic.Int32, handler) { 134 | var count atomic.Int32 135 | return &count, methods.errorIf(func() bool { 136 | count.Inc() 137 | return false 138 | }) 139 | } 140 | 141 | func echoServer(t *testing.T, method string, overrideResp []byte) string { 142 | s := newServer(t) 143 | if overrideResp != nil { 144 | s.register(fooMethod, methods.customArg3(overrideResp)) 145 | } else { 146 | s.register(fooMethod, methods.echo()) 147 | } 148 | 149 | return s.hostPort() 150 | } 151 | -------------------------------------------------------------------------------- /sorted/mapkeys.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package sorted contains sort related utilities. 22 | package sorted 23 | 24 | import ( 25 | "reflect" 26 | "sort" 27 | ) 28 | 29 | // MapKeys returns the keys of a map in sorted order. 30 | // Note: It currently only supports map[string]*. 31 | func MapKeys(m interface{}) []string { 32 | v := reflect.ValueOf(m) 33 | if v.IsNil() || v.Type().Key().Kind() != reflect.String { 34 | panic("sortedKeys expected non-nil map[string]") 35 | } 36 | 37 | keys := make([]string, 0, v.Len()) 38 | for _, v := range v.MapKeys() { 39 | keys = append(keys, v.Interface().(string)) 40 | } 41 | 42 | sort.Strings(keys) 43 | return keys 44 | } 45 | -------------------------------------------------------------------------------- /sorted/mapkeys_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package sorted 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestMapKeys(t *testing.T) { 30 | tests := []struct { 31 | m interface{} 32 | want []string 33 | }{ 34 | { 35 | m: map[string]string{}, 36 | want: []string{}, 37 | }, 38 | { 39 | m: map[string]int{ 40 | "b": 2, 41 | "c": 3, 42 | "a": 1, 43 | }, 44 | want: []string{"a", "b", "c"}, 45 | }, 46 | { 47 | m: map[string]string{ 48 | "b": "2", 49 | "c": "3", 50 | "a": "1", 51 | }, 52 | want: []string{"a", "b", "c"}, 53 | }, 54 | } 55 | 56 | for _, tt := range tests { 57 | got := MapKeys(tt.m) 58 | assert.Equal(t, tt.want, got, "MapKeys result mismatch") 59 | } 60 | } 61 | 62 | func TestMapKeysInvalidType(t *testing.T) { 63 | tests := []struct { 64 | msg string 65 | v interface{} 66 | }{ 67 | {"*int", new(int)}, 68 | {"string", "test"}, 69 | {"struct", struct{}{}}, 70 | {"*struct", &struct{}{}}, 71 | {"invalid map type (int key)", map[int]string{1: "a"}}, 72 | } 73 | 74 | for _, tt := range tests { 75 | assert.Panics(t, func() { 76 | MapKeys(tt.v) 77 | }, "%v: %v", tt.msg, tt.v) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /statsd/dual.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import "time" 4 | 5 | type multiClient []Client 6 | 7 | // MultiClient combines multiple Clients into a single Client. 8 | func MultiClient(clients ...Client) Client { 9 | return multiClient(clients) 10 | } 11 | 12 | func (mc multiClient) Inc(stat string) { 13 | for _, c := range mc { 14 | c.Inc(stat) 15 | } 16 | } 17 | 18 | func (mc multiClient) Timing(stat string, d time.Duration) { 19 | for _, c := range mc { 20 | c.Timing(stat, d) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /statsd/dual_test.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/uber/tchannel-go/testutils" 11 | "github.com/yarpc/yab/statsd/statsdtest" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func TestMulti(t *testing.T) { 16 | origUserEnv := os.Getenv("USER") 17 | defer os.Setenv("USER", origUserEnv) 18 | os.Setenv("USER", "tester") 19 | 20 | s := statsdtest.NewServer(t) 21 | defer s.Close() 22 | 23 | c1, err := NewClient(zap.NewNop(), s.Addr().String(), "c1", "foo") 24 | require.NoError(t, err, "Failed to create client") 25 | 26 | c2, err := NewClient(zap.NewNop(), s.Addr().String(), "c2", "foo") 27 | require.NoError(t, err, "Failed to create client") 28 | 29 | mc := MultiClient(c1, c2) 30 | mc.Inc("c") 31 | mc.Timing("t", time.Millisecond) 32 | 33 | want := map[string]int{ 34 | "yab.tester.c1.foo.c": 1, 35 | "yab.tester.c2.foo.c": 1, 36 | 37 | "yab.tester.c1.foo.t": 1, 38 | "yab.tester.c2.foo.t": 1, 39 | } 40 | 41 | require.True(t, testutils.WaitFor(time.Second, func() bool { 42 | return len(s.Aggregated()) >= len(want) 43 | }), "did not receive expected stats") 44 | 45 | assert.Equal(t, want, s.Aggregated(), "unexpected stats") 46 | } 47 | -------------------------------------------------------------------------------- /statsd/prefix.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import "time" 4 | 5 | type prefixClient struct { 6 | client Client 7 | prefix string 8 | } 9 | 10 | // NewPrefixedClient wraps the provided client to add a prefix to all calls. 11 | func NewPrefixedClient(client Client, prefix string) Client { 12 | return &prefixClient{ 13 | client: client, 14 | prefix: prefix, 15 | } 16 | } 17 | 18 | func (pc prefixClient) Inc(stat string) { 19 | pc.client.Inc(pc.prefix + stat) 20 | } 21 | 22 | func (pc prefixClient) Timing(stat string, d time.Duration) { 23 | pc.client.Timing(pc.prefix+stat, d) 24 | } 25 | -------------------------------------------------------------------------------- /statsd/prefix_test.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/uber/tchannel-go/testutils" 11 | "github.com/yarpc/yab/statsd/statsdtest" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func TestPrefixClient(t *testing.T) { 16 | origUserEnv := os.Getenv("USER") 17 | defer os.Setenv("USER", origUserEnv) 18 | os.Setenv("USER", "tester") 19 | 20 | s := statsdtest.NewServer(t) 21 | defer s.Close() 22 | 23 | c1, err := NewClient(zap.NewNop(), s.Addr().String(), "c1", "foo") 24 | require.NoError(t, err, "Failed to create client") 25 | 26 | c2 := NewPrefixedClient(c1, "prefix.") 27 | 28 | c1.Inc("c") 29 | c2.Inc("c") 30 | 31 | c1.Timing("t", time.Millisecond) 32 | c2.Timing("t", time.Millisecond) 33 | 34 | want := map[string]int{ 35 | "yab.tester.c1.foo.c": 1, 36 | "yab.tester.c1.foo.prefix.c": 1, 37 | 38 | "yab.tester.c1.foo.t": 1, 39 | "yab.tester.c1.foo.prefix.t": 1, 40 | } 41 | 42 | require.True(t, testutils.WaitFor(time.Second, func() bool { 43 | return len(s.Aggregated()) >= len(want) 44 | }), "did not receive expected stats") 45 | 46 | assert.Equal(t, want, s.Aggregated(), "unexpected stats") 47 | } 48 | -------------------------------------------------------------------------------- /statsd/statsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package statsd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "regexp" 27 | "time" 28 | 29 | "github.com/cactus/go-statsd-client/v5/statsd" 30 | "go.uber.org/zap" 31 | ) 32 | 33 | // Client is the statsd interface that we use. 34 | type Client interface { 35 | Inc(stat string) 36 | Timing(stat string, d time.Duration) 37 | } 38 | 39 | // Noop is a no-op statsd client. 40 | var Noop Client = client{newNoopStatter()} 41 | 42 | func newNoopStatter() statsd.Statter { 43 | // A nil *statsd.Client is a valid no-op statsd.Statter. 44 | // This will convert to a valid non-nil statsd.Statter value. 45 | var client *statsd.Client 46 | return client 47 | } 48 | 49 | var newStatsD = statsd.NewBufferedClient 50 | 51 | type client struct { 52 | statter statsd.Statter 53 | } 54 | 55 | func (c client) Inc(stat string) { 56 | c.statter.Inc(stat, 1, 1.0) 57 | } 58 | 59 | func (c client) Timing(stat string, d time.Duration) { 60 | c.statter.TimingDuration(stat, d, 1.0) 61 | } 62 | 63 | func createStatsd(statsdHostPort, service, method string) (statsd.Statter, error) { 64 | user := os.Getenv("USER") 65 | r := regexp.MustCompile(`[^a-zA-Z0-9]`) 66 | service = r.ReplaceAllString(service, "-") 67 | method = r.ReplaceAllString(method, "-") 68 | user = r.ReplaceAllString(user, "-") 69 | 70 | prefix := fmt.Sprintf("yab.%v.%v.%v", user, service, method) 71 | return newStatsD(statsdHostPort, prefix, 300*time.Millisecond, 0) 72 | } 73 | 74 | // NewClient returns a Client that sends metrics to statsd. 75 | func NewClient(logger *zap.Logger, statsdHostPort, service, method string) (Client, error) { 76 | if statsdHostPort == "" { 77 | return Noop, nil 78 | } 79 | 80 | logger.Debug("Create statsd client.", 81 | zap.String("hostPort", statsdHostPort), 82 | zap.String("serviceName", service), 83 | zap.String("procedure", method), 84 | ) 85 | statsd, err := createStatsd(statsdHostPort, service, method) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return client{statsd}, err 91 | } 92 | -------------------------------------------------------------------------------- /statsd/statsd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package statsd 22 | 23 | import ( 24 | "errors" 25 | "os" 26 | "testing" 27 | "time" 28 | 29 | "go.uber.org/zap" 30 | 31 | "github.com/cactus/go-statsd-client/v5/statsd" 32 | "github.com/stretchr/testify/assert" 33 | "github.com/stretchr/testify/require" 34 | ) 35 | 36 | var _testLogger = zap.NewNop() 37 | 38 | func TestNewClientEmpty(t *testing.T) { 39 | origNewStatsD := newStatsD 40 | defer func() { newStatsD = origNewStatsD }() 41 | 42 | newStatsD = func(addr string, prefix string, flushInterval time.Duration, flushBytes int) (statsd.Statter, error) { 43 | t.Errorf("newStatsD called when no statsd host:port was configured") 44 | return nil, nil 45 | } 46 | 47 | statsd, err := NewClient(_testLogger, "", "svc", "method") 48 | require.NoError(t, err, "NewClient with empty address should not fail") 49 | assert.NotNil(t, statsd, "NewClient with empty address should get client") 50 | 51 | // Ensure nop client doesn't cause any panics. 52 | statsd.Inc("c") 53 | statsd.Timing("t", time.Second) 54 | } 55 | 56 | func TestNewClientCreate(t *testing.T) { 57 | origNewStatsD := newStatsD 58 | defer func() { newStatsD = origNewStatsD }() 59 | origUserEnv := os.Getenv("USER") 60 | defer os.Setenv("USER", origUserEnv) 61 | 62 | noopClient := newNoopStatter() 63 | errFailed := errors.New("failure for test") 64 | 65 | tests := []struct { 66 | retStatter statsd.Statter 67 | retErr error 68 | wantErr error 69 | }{ 70 | {noopClient, nil, nil}, 71 | {noopClient, errFailed, errFailed}, 72 | {nil, errFailed, errFailed}, 73 | } 74 | 75 | for _, tt := range tests { 76 | os.Setenv("USER", "te.=?ster") 77 | newStatsD = func(addr string, prefix string, flushInterval time.Duration, flushBytes int) (statsd.Statter, error) { 78 | assert.Equal(t, "1.1.1.1:1", addr, "statsd host:port") 79 | assert.Equal(t, "yab.te---ster.s-v-c.m--ethod", prefix, "statsd prefix") 80 | assert.Equal(t, 300*time.Millisecond, flushInterval, "statsd flush interval") 81 | return tt.retStatter, tt.retErr 82 | } 83 | 84 | statsd, err := NewClient(_testLogger, "1.1.1.1:1", "s?v-c", "m:\"ethod") 85 | assert.Equal(t, tt.wantErr, err, "Expected failure from newStatsD") 86 | if tt.wantErr != nil { 87 | assert.Nil(t, statsd, "No client should be returned on error") 88 | } else { 89 | assert.NotNil(t, statsd, "Expected client to be returned") 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /statsd/statsdtest/server.go: -------------------------------------------------------------------------------- 1 | package statsdtest 2 | 3 | import ( 4 | "math" 5 | "net" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/cactus/go-statsd-client/v5/statsd/statsdtest" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // Server represents an in-memory statsd server for testing. 15 | type Server struct { 16 | t *testing.T 17 | ln net.PacketConn 18 | sender *statsdtest.RecordingSender 19 | running chan struct{} 20 | } 21 | 22 | // NewServer creates a new in-memory statsd server for testing. 23 | func NewServer(t *testing.T) *Server { 24 | sender := statsdtest.NewRecordingSender() 25 | 26 | ln, err := net.ListenPacket("udp", "127.0.0.1:0") 27 | require.NoError(t, err, "Failed to create UDP listener") 28 | 29 | s := &Server{ 30 | t: t, 31 | ln: ln, 32 | sender: sender, 33 | running: make(chan struct{}), 34 | } 35 | 36 | go s.run() 37 | return s 38 | } 39 | 40 | func (s *Server) run() { 41 | defer close(s.running) 42 | 43 | // UDP uses 16 bits for the length, so this is the max packet size. 44 | buf := make([]byte, math.MaxUint16) 45 | 46 | for { 47 | n, _, err := s.ln.ReadFrom(buf) 48 | if nerr, ok := err.(net.Error); ok && !nerr.Temporary() { 49 | return 50 | } 51 | 52 | _, err = s.sender.Send(buf[:n]) 53 | assert.NoError(s.t, err, "Failed to Send to recording sender") 54 | } 55 | } 56 | 57 | // Addr returns the net.Addr that the UDP server is listening on. 58 | func (s *Server) Addr() net.Addr { 59 | return s.ln.LocalAddr() 60 | } 61 | 62 | // Close stops the listener. 63 | func (s *Server) Close() { 64 | s.ln.Close() 65 | <-s.running 66 | } 67 | 68 | // Stats returns all stats that have been received by this server, keeping only Stat and Value. 69 | func (s *Server) Stats() statsdtest.Stats { 70 | rawStats := s.sender.GetSent() 71 | stats := make(statsdtest.Stats, len(rawStats)) 72 | for i, s := range rawStats { 73 | stats[i] = statsdtest.Stat{ 74 | Stat: s.Stat, 75 | Value: s.Value, 76 | } 77 | } 78 | return stats 79 | } 80 | 81 | // Aggregated returns an aggregated stats value. 82 | func (s *Server) Aggregated() map[string]int { 83 | aggregated := make(map[string]int) 84 | for _, stat := range s.sender.GetSent() { 85 | 86 | switch stat.Tag { 87 | case "c", "g": 88 | v, err := strconv.Atoi(stat.Value) 89 | require.NoError(s.t, err, "failed to convert %v: %v", stat.Stat, stat.Value) 90 | 91 | if stat.Tag == "c" { 92 | aggregated[stat.Stat] += v 93 | } else { 94 | aggregated[stat.Stat] = v 95 | } 96 | case "ms": 97 | aggregated[stat.Stat]++ 98 | } 99 | } 100 | 101 | return aggregated 102 | } 103 | -------------------------------------------------------------------------------- /statsd/statsdtest/server_test.go: -------------------------------------------------------------------------------- 1 | package statsdtest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/uber/tchannel-go/testutils" 8 | 9 | "github.com/cactus/go-statsd-client/v5/statsd" 10 | "github.com/cactus/go-statsd-client/v5/statsd/statsdtest" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "go.uber.org/multierr" 14 | ) 15 | 16 | func TestServer(t *testing.T) { 17 | server := NewServer(t) 18 | defer server.Close() 19 | 20 | client, err := statsd.NewClient(server.Addr().String(), "client") 21 | require.NoError(t, err, "failed to create client") 22 | 23 | err = multierr.Combine( 24 | client.Inc("counter", 1, 1.0), 25 | client.Inc("counter", 2, 1.0), 26 | client.Gauge("gauge", 3, 1.0), 27 | client.Gauge("gauge", 5, 1.0), 28 | client.Timing("timer", 100, 1.0), 29 | client.Timing("timer", 150, 1.0), 30 | ) 31 | require.NoError(t, err, "failed to emit metrics") 32 | 33 | want := statsdtest.Stats{ 34 | {Stat: "client.counter", Value: "1"}, 35 | {Stat: "client.counter", Value: "2"}, 36 | {Stat: "client.gauge", Value: "3"}, 37 | {Stat: "client.gauge", Value: "5"}, 38 | {Stat: "client.timer", Value: "100"}, 39 | {Stat: "client.timer", Value: "150"}, 40 | } 41 | aggregated := map[string]int{ 42 | "client.counter": 3, // counters should be summed 43 | "client.gauge": 5, // last gauge wins 44 | "client.timer": 2, // timers only store counts 45 | } 46 | 47 | // Wait for the server to receive all the stats. 48 | require.True(t, testutils.WaitFor(time.Second, func() bool { 49 | return len(server.Stats()) == len(want) 50 | }), "did not receive expected stats") 51 | 52 | assert.Equal(t, want, server.Stats(), "unexpected stats") 53 | assert.Equal(t, aggregated, server.Aggregated(), "unexpected aggregated stats") 54 | } 55 | -------------------------------------------------------------------------------- /statsd_fake_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "sync" 25 | "time" 26 | 27 | "github.com/yarpc/yab/statsd" 28 | ) 29 | 30 | type fakeStatsd struct { 31 | sync.Mutex 32 | 33 | Counters map[string]int 34 | Timers map[string][]time.Duration 35 | } 36 | 37 | var _ statsd.Client = newFakeStatsClient() 38 | 39 | func newFakeStatsClient() *fakeStatsd { 40 | return &fakeStatsd{ 41 | Counters: make(map[string]int), 42 | Timers: make(map[string][]time.Duration), 43 | } 44 | } 45 | 46 | func (f *fakeStatsd) Inc(stat string) { 47 | f.Lock() 48 | defer f.Unlock() 49 | 50 | f.Counters[stat]++ 51 | } 52 | 53 | func (f *fakeStatsd) Timing(stat string, d time.Duration) { 54 | f.Lock() 55 | defer f.Unlock() 56 | 57 | f.Timers[stat] = append(f.Timers[stat], d) 58 | } 59 | -------------------------------------------------------------------------------- /templateargs/interpolate/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package interpolate 22 | 23 | //go:generate ragel -Z -G2 -o parse.go parse.rl 24 | //go:generate gofmt -s -w parse.go 25 | //go:generate ./generated.sh 26 | -------------------------------------------------------------------------------- /templateargs/interpolate/generated.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # We filter out //line directives because golang/go#27350 affects us in 4 | # Go 1.11. 5 | echo -e "// Code generated by ragel\n// @generated" \ 6 | | cat - parse.go \ 7 | | grep -v "^//line" \ 8 | > tmp 9 | mv tmp parse.go 10 | -------------------------------------------------------------------------------- /templateargs/interpolate/parse.rl: -------------------------------------------------------------------------------- 1 | package interpolate 2 | 3 | import "fmt" 4 | 5 | %%{ 6 | machine interpolate; 7 | write data; 8 | }%% 9 | 10 | // Parse parses a string for interpolation. 11 | // 12 | // Variables may be specified anywhere in the string in the format ${foo} or 13 | // ${foo:default} where 'default' will be used if the variable foo was unset. 14 | func Parse(data string) (out String, _ error) { 15 | var ( 16 | // Ragel variables 17 | cs = 0 18 | p = 0 19 | pe = len(data) 20 | eof = pe 21 | 22 | // The following variables are used by us to build String up. 23 | 24 | // Index in data where the currently captured string started. 25 | idx int 26 | 27 | // Variable currently being built. 28 | v variable 29 | 30 | // Literal currently being read. 31 | l literal 32 | 33 | // Last read term (variable or literal) which we will append to the 34 | // output. 35 | t term 36 | ) 37 | 38 | %%{ 39 | # Record the current position as the start of a string. 40 | action start { idx = fpc } 41 | 42 | var_name 43 | = ([a-zA-Z_] ([a-zA-Z0-9_] | ('.' | '-') [a-zA-Z0-9_])*) 44 | >start 45 | @{ v.Name = data[idx:fpc+1] }; 46 | 47 | var_default 48 | = (any - '}')* >start @{ v.Default = data[idx:fpc+1] }; 49 | 50 | # Reference to a variable with an optional default value. 51 | var = '${' var_name (':' @{ v.HasDefault = true } var_default)? '}' 52 | ; 53 | 54 | # Anything followed by a '\' is used as-is. 55 | escaped_lit = '\\' any @{ l = literal(data[fpc:fpc+1]) }; 56 | 57 | # Anything followed by a '$' that is not a '{'. 58 | dollar_lit = '$' (any - '{') @{ l = literal(data[fpc-1:fpc+1]) }; 59 | 60 | # Literal strings that don't contain '$' or '\'. 61 | simple_lit 62 | = (any - '$' - '\\')+ 63 | >start 64 | @{ l = literal(data[idx:fpc + 1]) } 65 | ; 66 | 67 | lit = escaped_lit | dollar_lit | simple_lit; 68 | 69 | term = (var @{ t = v }) | (lit @{ t = l }); 70 | 71 | main := (term %{ out = append(out, t) })**; 72 | 73 | write init; 74 | write exec; 75 | }%% 76 | 77 | if cs < %%{ write first_final; }%% { 78 | return out, fmt.Errorf("cannot parse string %q", data) 79 | } 80 | 81 | return out, nil 82 | } 83 | -------------------------------------------------------------------------------- /templateargs/interpolate/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package interpolate 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestParseSuccess(t *testing.T) { 31 | tests := []struct { 32 | give string 33 | want String 34 | }{ 35 | { 36 | give: "foo", 37 | want: String{literal("foo")}, 38 | }, 39 | { 40 | give: "foo ${bar} baz", 41 | want: String{ 42 | literal("foo "), 43 | variable{Name: "bar"}, 44 | literal(" baz"), 45 | }, 46 | }, 47 | { 48 | give: "foo ${bar:}", 49 | want: String{ 50 | literal("foo "), 51 | variable{Name: "bar", HasDefault: true}, 52 | }, 53 | }, 54 | { 55 | give: "${foo:bar}", 56 | want: String{ 57 | variable{ 58 | Name: "foo", 59 | Default: "bar", 60 | HasDefault: true, 61 | }, 62 | }, 63 | }, 64 | { 65 | give: `foo \${bar:42} baz`, 66 | want: String{ 67 | literal("foo "), 68 | literal("$"), 69 | literal("{bar:42} baz"), 70 | }, 71 | }, 72 | { 73 | give: "$foo${bar}", 74 | want: String{ 75 | literal("$f"), 76 | literal("oo"), 77 | variable{Name: "bar"}, 78 | }, 79 | }, 80 | { 81 | give: "foo${b-a-r}", 82 | want: String{ 83 | literal("foo"), 84 | variable{Name: "b-a-r"}, 85 | }, 86 | }, 87 | } 88 | 89 | for _, tt := range tests { 90 | out, err := Parse(tt.give) 91 | assert.NoError(t, err) 92 | assert.Equal(t, tt.want, out) 93 | } 94 | } 95 | 96 | func TestParseFailures(t *testing.T) { 97 | tests := []string{ 98 | "${foo", 99 | "${foo.}", 100 | "${foo-}", 101 | } 102 | 103 | for _, tt := range tests { 104 | _, err := Parse(tt) 105 | require.Error(t, err, tt) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /templateargs/interpolate/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package interpolate 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "io" 27 | "os" 28 | ) 29 | 30 | // We represent the user-defined string as a series of terms. Each term is 31 | // either a literal or a variable. Literals are used as-is and variables are 32 | // resolved using a VariableResolver. 33 | type ( 34 | term interface { 35 | term() 36 | } 37 | 38 | literal string 39 | 40 | variable struct { 41 | Name string 42 | Default string 43 | HasDefault bool 44 | } 45 | ) 46 | 47 | func (literal) term() {} 48 | func (variable) term() {} 49 | 50 | // VariableResolver resolves the value of a variable specified in the string. 51 | // 52 | // The boolean value indicates whether this variable had a value defined. If a 53 | // variable does not have a value and no default is specified, rendering will 54 | // fail. 55 | type VariableResolver func(name string) (value string, ok bool) 56 | 57 | // EnvResolver is a VariableResolver that maps every variable to an 58 | // environment variable. 59 | var EnvResolver = VariableResolver(os.LookupEnv) 60 | 61 | // String is a string that supports interpolation given some source of 62 | // variable values. 63 | type String []term 64 | 65 | // Render renders and returns the string. The provided VariableResolver will 66 | // be used to determine values for the different variables mentioned in the 67 | // string. 68 | func (s String) Render(resolve VariableResolver) (string, error) { 69 | var buff bytes.Buffer 70 | if err := s.RenderTo(&buff, resolve); err != nil { 71 | return "", err 72 | } 73 | return buff.String(), nil 74 | } 75 | 76 | // RenderTo renders the string into the given writer. The provided 77 | // VariableResolver will be used to determine values for the different 78 | // variables mentioned in the string. 79 | func (s String) RenderTo(w io.Writer, resolve VariableResolver) error { 80 | for _, term := range s { 81 | var value string 82 | switch t := term.(type) { 83 | case literal: 84 | value = string(t) 85 | case variable: 86 | if val, ok := resolve(t.Name); ok { 87 | value = val 88 | } else if t.HasDefault { 89 | value = t.Default 90 | } else { 91 | return errUnknownVariable{Name: t.Name} 92 | } 93 | } 94 | if _, err := io.WriteString(w, value); err != nil { 95 | return err 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | type errUnknownVariable struct{ Name string } 102 | 103 | func (e errUnknownVariable) Error() string { 104 | return fmt.Sprintf("unknown variable %q does not have a value or a default", e.Name) 105 | } 106 | -------------------------------------------------------------------------------- /templateargs/interpolate/types_test.go: -------------------------------------------------------------------------------- 1 | package interpolate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func mapResolver(m map[string]string) VariableResolver { 10 | return func(name string) (value string, ok bool) { 11 | if m == nil { 12 | return "", false 13 | } 14 | value, ok = m[name] 15 | return 16 | } 17 | } 18 | 19 | func TestRender(t *testing.T) { 20 | tests := []struct { 21 | give String 22 | vars map[string]string 23 | 24 | want string 25 | wantErr string 26 | }{ 27 | { 28 | give: String{literal("foo "), literal("bar")}, 29 | want: "foo bar", 30 | }, 31 | { 32 | give: String{literal("foo"), variable{Name: "bar"}}, 33 | vars: map[string]string{"bar": "baz"}, 34 | want: "foobaz", 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | got, err := tt.give.Render(mapResolver(tt.vars)) 40 | if tt.wantErr != "" { 41 | if assert.Error(t, err) { 42 | assert.Contains(t, err.Error(), tt.wantErr) 43 | } 44 | continue 45 | } 46 | 47 | if assert.NoError(t, err) { 48 | assert.Equal(t, tt.want, got) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /templateargs/process.go: -------------------------------------------------------------------------------- 1 | package templateargs 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/yarpc/yab/templateargs/interpolate" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | // ProcessMap takes a YAML request that may contain values like ${name:prashant} 12 | // and replaces any template arguments with those specified in args. 13 | func ProcessMap(req map[interface{}]interface{}, args map[string]string) (map[interface{}]interface{}, error) { 14 | return processMap(req, args) 15 | } 16 | 17 | func processString(v string, args map[string]string) (interface{}, error) { 18 | parsed, err := interpolate.Parse(v) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | rendered, err := parsed.Render(func(name string) (value string, ok bool) { 24 | v, ok := args[name] 25 | return v, ok 26 | }) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if rendered == "" { 32 | return "", nil 33 | } 34 | if rendered == v { 35 | // Avoid unmarshalling if the value did not change. 36 | return v, nil 37 | } 38 | 39 | // Otherwise, unmarshal the value and return that. 40 | var unmarshalled interface{} 41 | err = yaml.Unmarshal([]byte(rendered), &unmarshalled) 42 | 43 | if _, isBool := unmarshalled.(bool); isBool { 44 | // The Go YAML parser has some unfortunate handling of strings that look 45 | // like booleans: https://github.com/go-yaml/yaml/issues/214 46 | // Let's use a more strict definition for booleans. 47 | if _, err := strconv.ParseBool(rendered); err != nil { 48 | // Go doesn't think this is a boolean, so use the value as a string 49 | return rendered, nil 50 | } 51 | } 52 | 53 | return unmarshalled, err 54 | } 55 | 56 | func processValue(v interface{}, args map[string]string) (interface{}, error) { 57 | switch v := v.(type) { 58 | case string: 59 | return processString(v, args) 60 | case map[interface{}]interface{}: 61 | return processMap(v, args) 62 | case []interface{}: 63 | return processList(v, args) 64 | default: 65 | return v, nil 66 | } 67 | 68 | } 69 | 70 | func processList(l []interface{}, args map[string]string) ([]interface{}, error) { 71 | replacement := make([]interface{}, len(l)) 72 | for i, v := range l { 73 | newV, err := processValue(v, args) 74 | if err != nil { 75 | return nil, err 76 | } 77 | replacement[i] = newV 78 | } 79 | 80 | return replacement, nil 81 | } 82 | 83 | func processMap(m map[interface{}]interface{}, args map[string]string) (map[interface{}]interface{}, error) { 84 | replacement := make(map[interface{}]interface{}, len(m)) 85 | for k, v := range m { 86 | newK, err := processValue(k, args) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | newV, err := processValue(v, args) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | replacement[newK] = newV 97 | } 98 | 99 | return replacement, nil 100 | } 101 | -------------------------------------------------------------------------------- /test.bin: -------------------------------------------------------------------------------- 1 | 2 | I 3 | 4 | test.proto" 5 | Foo 6 | test (Rtest2 7 | Bar 8 | Baz.Foo.Foobproto3 -------------------------------------------------------------------------------- /testdata/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarpc/yab/c16a5d2148de2894ad0325cf7cfb5f738927c710/testdata/empty.txt -------------------------------------------------------------------------------- /testdata/gen-go/integration/constants.go: -------------------------------------------------------------------------------- 1 | // Autogenerated by Thrift Compiler (1.0.0-dev) 2 | // DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING 3 | 4 | package integration 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "github.com/uber/tchannel-go/thirdparty/github.com/apache/thrift/lib/go/thrift" 10 | ) 11 | 12 | // (needed to ensure safety because of naive import list construction.) 13 | var _ = thrift.ZERO 14 | var _ = fmt.Printf 15 | var _ = bytes.Equal 16 | 17 | func init() { 18 | } 19 | -------------------------------------------------------------------------------- /testdata/gen-go/integration/tchan-integration.go: -------------------------------------------------------------------------------- 1 | // @generated Code generated by thrift-gen. Do not modify. 2 | 3 | // Package integration is generated code used to make or handle TChannel calls using Thrift. 4 | package integration 5 | 6 | import ( 7 | "fmt" 8 | 9 | athrift "github.com/uber/tchannel-go/thirdparty/github.com/apache/thrift/lib/go/thrift" 10 | "github.com/uber/tchannel-go/thrift" 11 | ) 12 | 13 | // Interfaces for the service and client for the services defined in the IDL. 14 | 15 | // TChanFoo is the interface that defines the server handler and client interface. 16 | type TChanFoo interface { 17 | Bar(ctx thrift.Context, arg int32) (int32, error) 18 | } 19 | 20 | // Implementation of a client and service handler. 21 | 22 | type tchanFooClient struct { 23 | thriftService string 24 | client thrift.TChanClient 25 | } 26 | 27 | func NewTChanFooInheritedClient(thriftService string, client thrift.TChanClient) *tchanFooClient { 28 | return &tchanFooClient{ 29 | thriftService, 30 | client, 31 | } 32 | } 33 | 34 | // NewTChanFooClient creates a client that can be used to make remote calls. 35 | func NewTChanFooClient(client thrift.TChanClient) TChanFoo { 36 | return NewTChanFooInheritedClient("Foo", client) 37 | } 38 | 39 | func (c *tchanFooClient) Bar(ctx thrift.Context, arg int32) (int32, error) { 40 | var resp FooBarResult 41 | args := FooBarArgs{ 42 | Arg: arg, 43 | } 44 | success, err := c.client.Call(ctx, c.thriftService, "bar", &args, &resp) 45 | if err == nil && !success { 46 | if e := resp.NotFound; e != nil { 47 | err = e 48 | } 49 | } 50 | 51 | return resp.GetSuccess(), err 52 | } 53 | 54 | type tchanFooServer struct { 55 | handler TChanFoo 56 | } 57 | 58 | // NewTChanFooServer wraps a handler for TChanFoo so it can be 59 | // registered with a thrift.Server. 60 | func NewTChanFooServer(handler TChanFoo) thrift.TChanServer { 61 | return &tchanFooServer{ 62 | handler, 63 | } 64 | } 65 | 66 | func (s *tchanFooServer) Service() string { 67 | return "Foo" 68 | } 69 | 70 | func (s *tchanFooServer) Methods() []string { 71 | return []string{ 72 | "bar", 73 | } 74 | } 75 | 76 | func (s *tchanFooServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 77 | switch methodName { 78 | case "bar": 79 | return s.handleBar(ctx, protocol) 80 | 81 | default: 82 | return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service()) 83 | } 84 | } 85 | 86 | func (s *tchanFooServer) handleBar(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 87 | var req FooBarArgs 88 | var res FooBarResult 89 | 90 | if err := req.Read(protocol); err != nil { 91 | return false, nil, err 92 | } 93 | 94 | r, err := 95 | s.handler.Bar(ctx, req.Arg) 96 | 97 | if err != nil { 98 | switch v := err.(type) { 99 | case *NotFound: 100 | if v == nil { 101 | return false, nil, fmt.Errorf("Handler for notFound returned non-nil error type *NotFound but nil value") 102 | } 103 | res.NotFound = v 104 | default: 105 | return false, nil, err 106 | } 107 | } else { 108 | res.Success = &r 109 | } 110 | 111 | return err == nil, &res, nil 112 | } 113 | -------------------------------------------------------------------------------- /testdata/gen-go/integration/ttypes.go: -------------------------------------------------------------------------------- 1 | // Autogenerated by Thrift Compiler (1.0.0-dev) 2 | // DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING 3 | 4 | package integration 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "github.com/uber/tchannel-go/thirdparty/github.com/apache/thrift/lib/go/thrift" 10 | ) 11 | 12 | // (needed to ensure safety because of naive import list construction.) 13 | var _ = thrift.ZERO 14 | var _ = fmt.Printf 15 | var _ = bytes.Equal 16 | 17 | var GoUnusedProtection__ int 18 | 19 | type NotFound struct { 20 | } 21 | 22 | func NewNotFound() *NotFound { 23 | return &NotFound{} 24 | } 25 | 26 | func (p *NotFound) Read(iprot thrift.TProtocol) error { 27 | if _, err := iprot.ReadStructBegin(); err != nil { 28 | return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) 29 | } 30 | 31 | for { 32 | _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin() 33 | if err != nil { 34 | return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) 35 | } 36 | if fieldTypeId == thrift.STOP { 37 | break 38 | } 39 | if err := iprot.Skip(fieldTypeId); err != nil { 40 | return err 41 | } 42 | if err := iprot.ReadFieldEnd(); err != nil { 43 | return err 44 | } 45 | } 46 | if err := iprot.ReadStructEnd(); err != nil { 47 | return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) 48 | } 49 | return nil 50 | } 51 | 52 | func (p *NotFound) Write(oprot thrift.TProtocol) error { 53 | if err := oprot.WriteStructBegin("NotFound"); err != nil { 54 | return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) 55 | } 56 | if err := oprot.WriteFieldStop(); err != nil { 57 | return thrift.PrependError("write field stop error: ", err) 58 | } 59 | if err := oprot.WriteStructEnd(); err != nil { 60 | return thrift.PrependError("write struct stop error: ", err) 61 | } 62 | return nil 63 | } 64 | 65 | func (p *NotFound) String() string { 66 | if p == nil { 67 | return "" 68 | } 69 | return fmt.Sprintf("NotFound(%+v)", *p) 70 | } 71 | 72 | func (p *NotFound) Error() string { 73 | return p.String() 74 | } 75 | -------------------------------------------------------------------------------- /testdata/grpc_health.yab: -------------------------------------------------------------------------------- 1 | service: yab-grpc-test 2 | peer: PEERHOSTPORT 3 | method: "grpc.health.v1.Health/Check" 4 | request: 5 | service: yab-grpc-test 6 | -------------------------------------------------------------------------------- /testdata/ini/invalid/yab/defaults.ini: -------------------------------------------------------------------------------- 1 | [request] 2 | timeout = 3foo 3 | -------------------------------------------------------------------------------- /testdata/ini/valid/yab/defaults.ini: -------------------------------------------------------------------------------- 1 | [request] 2 | timeout = 2s 3 | -------------------------------------------------------------------------------- /testdata/integration.thrift: -------------------------------------------------------------------------------- 1 | exception NotFound {} 2 | 3 | service Foo { 4 | i32 bar(1: i32 arg) throws (1: NotFound notFound) 5 | } 6 | -------------------------------------------------------------------------------- /testdata/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | -------------------------------------------------------------------------------- /testdata/invalid_peerlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.1.1.1:1": "key" 3 | } 4 | -------------------------------------------------------------------------------- /testdata/invalid_url_peerlist.txt: -------------------------------------------------------------------------------- 1 | cache_object:foo/b:ar 2 | -------------------------------------------------------------------------------- /testdata/protobuf/any/any.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: any.proto 3 | // DO NOT EDIT! 4 | 5 | /* 6 | Package any is a generated protocol buffer package. 7 | 8 | It is generated from these files: 9 | any.proto 10 | 11 | It has these top-level messages: 12 | FooAny 13 | */ 14 | package any 15 | 16 | import proto "github.com/golang/protobuf/proto" 17 | import fmt "fmt" 18 | import math "math" 19 | import google_protobuf "github.com/golang/protobuf/ptypes/any" 20 | 21 | // Reference imports to suppress errors if they are not otherwise used. 22 | var _ = proto.Marshal 23 | var _ = fmt.Errorf 24 | var _ = math.Inf 25 | 26 | // This is a compile-time assertion to ensure that this generated file 27 | // is compatible with the proto package it is being compiled against. 28 | // A compilation error at this line likely means your copy of the 29 | // proto package needs to be updated. 30 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 31 | 32 | type FooAny struct { 33 | Value int32 `protobuf:"varint,1,opt,name=value" json:"value,omitempty"` 34 | NestedAny *google_protobuf.Any `protobuf:"bytes,2,opt,name=nestedAny" json:"nestedAny,omitempty"` 35 | } 36 | 37 | func (m *FooAny) Reset() { *m = FooAny{} } 38 | func (m *FooAny) String() string { return proto.CompactTextString(m) } 39 | func (*FooAny) ProtoMessage() {} 40 | func (*FooAny) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 41 | 42 | func (m *FooAny) GetValue() int32 { 43 | if m != nil { 44 | return m.Value 45 | } 46 | return 0 47 | } 48 | 49 | func (m *FooAny) GetNestedAny() *google_protobuf.Any { 50 | if m != nil { 51 | return m.NestedAny 52 | } 53 | return nil 54 | } 55 | 56 | func init() { 57 | proto.RegisterType((*FooAny)(nil), "FooAny") 58 | } 59 | 60 | func init() { proto.RegisterFile("any.proto", fileDescriptor0) } 61 | 62 | var fileDescriptor0 = []byte{ 63 | // 140 bytes of a gzipped FileDescriptorProto 64 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0xcc, 0xab, 0xd4, 65 | 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x97, 0x92, 0x4c, 0xcf, 0xcf, 0x4f, 0xcf, 0x49, 0xd5, 0x07, 0xf3, 66 | 0x92, 0x4a, 0xd3, 0xf4, 0xe1, 0x52, 0x4a, 0x41, 0x5c, 0x6c, 0x6e, 0xf9, 0xf9, 0x8e, 0x79, 0x95, 67 | 0x42, 0x22, 0x5c, 0xac, 0x65, 0x89, 0x39, 0xa5, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xac, 0x41, 68 | 0x10, 0x8e, 0x90, 0x11, 0x17, 0x67, 0x5e, 0x6a, 0x71, 0x49, 0x6a, 0x8a, 0x63, 0x5e, 0xa5, 0x04, 69 | 0x93, 0x02, 0xa3, 0x06, 0xb7, 0x91, 0x88, 0x1e, 0xc4, 0x38, 0x3d, 0x98, 0x71, 0x7a, 0x8e, 0x79, 70 | 0x95, 0x41, 0x08, 0x65, 0x46, 0x2a, 0x5c, 0x6c, 0x4e, 0x89, 0x45, 0x20, 0x33, 0xa5, 0x40, 0xac, 71 | 0x2a, 0x10, 0x8b, 0x5d, 0x0f, 0x62, 0x8d, 0x14, 0x8c, 0x91, 0xc4, 0x06, 0xd6, 0x6e, 0x0c, 0x08, 72 | 0x00, 0x00, 0xff, 0xff, 0x89, 0x1c, 0xb1, 0x5f, 0xa8, 0x00, 0x00, 0x00, 73 | } 74 | -------------------------------------------------------------------------------- /testdata/protobuf/any/any.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/any.proto"; 4 | 5 | message FooAny { 6 | int32 value = 1; 7 | google.protobuf.Any nestedAny = 2; 8 | } 9 | 10 | service BarAny{ 11 | rpc BazAny(FooAny) returns (FooAny); 12 | } -------------------------------------------------------------------------------- /testdata/protobuf/any/any.proto.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarpc/yab/c16a5d2148de2894ad0325cf7cfb5f738927c710/testdata/protobuf/any/any.proto.bin -------------------------------------------------------------------------------- /testdata/protobuf/dependencies/combined.bin: -------------------------------------------------------------------------------- 1 | 2 | . 3 | dep.proto" 4 | Foo 5 | test (Rtestbproto3 6 | 9 7 | 8 | main.proto dep.proto2 9 | Bar 10 | Baz.Foo.Foobproto3 -------------------------------------------------------------------------------- /testdata/protobuf/dependencies/dep.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Foo { 4 | int32 test = 1; 5 | } 6 | -------------------------------------------------------------------------------- /testdata/protobuf/dependencies/dep.proto.bin: -------------------------------------------------------------------------------- 1 | 2 | . 3 | dep.proto" 4 | Foo 5 | test (Rtestbproto3 -------------------------------------------------------------------------------- /testdata/protobuf/dependencies/main.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "dep.proto"; 4 | 5 | service Bar{ 6 | rpc Baz(Foo) returns (Foo); 7 | } 8 | -------------------------------------------------------------------------------- /testdata/protobuf/dependencies/main.proto.bin: -------------------------------------------------------------------------------- 1 | 2 | 9 3 | 4 | main.proto dep.proto2 5 | Bar 6 | Baz.Foo.Foobproto3 -------------------------------------------------------------------------------- /testdata/protobuf/dependencies/other.bin: -------------------------------------------------------------------------------- 1 | 2 | . 3 | dep.proto" 4 | Fox 5 | test (Rtestbproto3 -------------------------------------------------------------------------------- /testdata/protobuf/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # generate compiled binary protodescriptors, protoc required then commit files for test usage 4 | # 5 | set -euo pipefail 6 | 7 | THIS_DIR="$(cd -P -- "$(dirname -- "$0")" && pwd -P)" 8 | 9 | ## simple 10 | # as expected 11 | (cd "$THIS_DIR/simple" && protoc -osimple.proto.bin simple.proto) 12 | 13 | 14 | ## dependencies 15 | # as expected 16 | (cd "$THIS_DIR/dependencies" && protoc --include_imports --descriptor_set_out=combined.bin main.proto) 17 | 18 | # no --include_imports 19 | (cd "$THIS_DIR/dependencies" && protoc --descriptor_set_out=main.proto.bin main.proto) 20 | 21 | # just the dependency to check using multiple precompiled files 22 | (cd "$THIS_DIR/dependencies" && protoc --descriptor_set_out=dep.proto.bin dep.proto) 23 | 24 | # search and replace message name Foo with fox, to simulate errors where proto files were named similarly but 25 | # have different contents 26 | (cd "$THIS_DIR/dependencies" && perl -p -e 's/Foo/Fox/g' dep.proto.bin > other.bin) 27 | 28 | 29 | ## multiroot 30 | # as expected 31 | (cd "$THIS_DIR/multiroot/root" && protoc \ 32 | --include_imports \ 33 | --descriptor_set_out=combined.bin \ 34 | -I "$THIS_DIR/multiroot/root" \ 35 | -I "$THIS_DIR/multiroot/other" \ 36 | main.proto) 37 | 38 | ## nested 39 | # as expected 40 | (cd "$THIS_DIR/nested" && protoc \ 41 | --include_imports \ 42 | --descriptor_set_out=combined.bin \ 43 | main.proto) 44 | 45 | ## any 46 | # as expected 47 | (cd "$THIS_DIR/any" && protoc \ 48 | --include_imports \ 49 | --descriptor_set_out=any.proto.bin \ 50 | any.proto) 51 | -------------------------------------------------------------------------------- /testdata/protobuf/multiroot/other/dep.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Foo { 4 | int32 test = 1; 5 | } 6 | -------------------------------------------------------------------------------- /testdata/protobuf/multiroot/root/combined.bin: -------------------------------------------------------------------------------- 1 | 2 | . 3 | dep.proto" 4 | Foo 5 | test (Rtestbproto3 6 | 9 7 | 8 | main.proto dep.proto2 9 | Bar 10 | Baz.Foo.Foobproto3 -------------------------------------------------------------------------------- /testdata/protobuf/multiroot/root/main.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "dep.proto"; 4 | 5 | service Bar{ 6 | rpc Baz(Foo) returns (Foo); 7 | } 8 | -------------------------------------------------------------------------------- /testdata/protobuf/nested/combined.bin: -------------------------------------------------------------------------------- 1 | 2 | 4 3 | other/dep.proto" 4 | Foo 5 | test (Rtestbproto3 6 | ? 7 | 8 | main.protoother/dep.proto2 9 | Bar 10 | Baz.Foo.Foobproto3 -------------------------------------------------------------------------------- /testdata/protobuf/nested/main.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "other/dep.proto"; 4 | 5 | service Bar{ 6 | rpc Baz(Foo) returns (Foo); 7 | } 8 | -------------------------------------------------------------------------------- /testdata/protobuf/nested/other/dep.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Foo { 4 | int32 test = 1; 5 | } 6 | -------------------------------------------------------------------------------- /testdata/protobuf/simple/simple.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Nested { 4 | int32 value = 1; 5 | } 6 | 7 | message Foo { 8 | int32 test = 1; 9 | Nested nested = 2; 10 | } 11 | 12 | service Bar{ 13 | rpc Baz(Foo) returns (Foo); 14 | rpc BidiStream(stream Foo) returns (stream Foo); 15 | rpc ClientStream(stream Foo) returns (Foo); 16 | rpc ServerStream(Foo) returns (stream Foo); 17 | } 18 | -------------------------------------------------------------------------------- /testdata/protobuf/simple/simple.proto.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarpc/yab/c16a5d2148de2894ad0325cf7cfb5f738927c710/testdata/protobuf/simple/simple.proto.bin -------------------------------------------------------------------------------- /testdata/simple.thrift: -------------------------------------------------------------------------------- 1 | exception ThriftException {} 2 | 3 | service Simple { 4 | void foo() 5 | i32 bar() 6 | void thriftEx() throws (1: ThriftException ex) 7 | 8 | void withDefault(1: set values = [1, 2, 3]) 9 | } 10 | -------------------------------------------------------------------------------- /testdata/templates/abspeerlist.yaml: -------------------------------------------------------------------------------- 1 | peerlist: /peers.json 2 | -------------------------------------------------------------------------------- /testdata/templates/args.yaml: -------------------------------------------------------------------------------- 1 | service: foo 2 | headers: 3 | header1: ${user:foo} 4 | request: 5 | emptyfallback: ${unspecified:} 6 | fallback: ${unspecified:fallback} 7 | fallbacklist: ${unspecified:[1,2]} 8 | noReplace: \${user} \\\${ 9 | replaceList: ${uuids} 10 | replaced: ${user:u} 11 | -------------------------------------------------------------------------------- /testdata/templates/bad-arg.yaml: -------------------------------------------------------------------------------- 1 | service: foo 2 | request: 3 | name: ${user:foo 4 | -------------------------------------------------------------------------------- /testdata/templates/foo-method.yaml: -------------------------------------------------------------------------------- 1 | method: Simple::foo 2 | -------------------------------------------------------------------------------- /testdata/templates/foo.thrift: -------------------------------------------------------------------------------- 1 | struct QueryLocation { 2 | 1: required double latitude 3 | 2: required double longitude 4 | 3: optional i32 cityId 5 | 4: optional string message 6 | 5: optional string user 7 | } 8 | 9 | service Simple { 10 | void foo(1: QueryLocation location) 11 | } 12 | -------------------------------------------------------------------------------- /testdata/templates/foo.yab: -------------------------------------------------------------------------------- 1 | service: foo 2 | procedure: Simple::foo 3 | caller: bar 4 | shardKey: sk 5 | routingKey: rk 6 | routingDelegate: rd 7 | thrift: foo.thrift 8 | timeout: 4.5s 9 | disableThriftEnvelope: true 10 | headers: 11 | header1: from template 12 | header3: overridden by Headers 13 | baggage: 14 | baggage1: value1 15 | baggage2: value2 16 | jaeger: true 17 | forceJaegerSample: true 18 | request: 19 | location: 20 | latitude: 37.7 21 | longitude: -122.4 22 | cityId: 1 23 | message: true 24 | -------------------------------------------------------------------------------- /testdata/templates/invalid-fields.yab: -------------------------------------------------------------------------------- 1 | service: foo 2 | procedure: Bar/BidiStream 3 | caller: bar 4 | timeout: 4.5s 5 | request: 6 | description: request and requests fields must not be set together 7 | requests: 8 | - test: 1 9 | - test: 2 10 | -------------------------------------------------------------------------------- /testdata/templates/invalid.yaml: -------------------------------------------------------------------------------- 1 | missing-quotes: " -------------------------------------------------------------------------------- /testdata/templates/peer.yaml: -------------------------------------------------------------------------------- 1 | service: foo 2 | procedure: Simple::foo 3 | peer: 127.0.0.1:8080 4 | peers: 5 | - 127.0.0.1:8080 6 | - 127.0.0.1:8081 7 | peer-list: peers.json 8 | caller: bar 9 | shardKey: sk 10 | routingKey: rk 11 | routingDelegate: rd 12 | thrift: foo.thrift 13 | timeout: 4.5s 14 | headers: 15 | header1: from template 16 | header3: overridden by Headers 17 | baggage: 18 | baggage1: value1 19 | baggage2: value2 20 | jaeger: true 21 | request: 22 | location: 23 | latitude: 37.7 24 | longitude: -122.4 25 | cityId: 1 26 | -------------------------------------------------------------------------------- /testdata/templates/peerlist.yaml: -------------------------------------------------------------------------------- 1 | peerList: peers.json 2 | -------------------------------------------------------------------------------- /testdata/templates/peers.json: -------------------------------------------------------------------------------- 1 | [ 2 | "127.0.0.1:8080", 3 | "127.0.0.1:8081", 4 | "127.0.0.1:8082" 5 | ] 6 | -------------------------------------------------------------------------------- /testdata/templates/peers.yaml: -------------------------------------------------------------------------------- 1 | peers: 2 | - 127.0.0.1:8080 3 | - 127.0.0.1:8081 4 | -------------------------------------------------------------------------------- /testdata/templates/stream.yab: -------------------------------------------------------------------------------- 1 | service: foo 2 | procedure: Bar/BidiStream 3 | caller: bar 4 | timeout: 4.5s 5 | requests: 6 | - test: 1 7 | - test: 2 8 | -------------------------------------------------------------------------------- /testdata/valid.json: -------------------------------------------------------------------------------- 1 | {"k1": "v1", "k2": 5} 2 | -------------------------------------------------------------------------------- /testdata/valid_peerlist.json: -------------------------------------------------------------------------------- 1 | ["1.1.1.1:1", "2.2.2.2:2"] 2 | -------------------------------------------------------------------------------- /testdata/valid_peerlist.txt: -------------------------------------------------------------------------------- 1 | 1.1.1.1:1 2 | 3 | 2.2.2.2:2 4 | -------------------------------------------------------------------------------- /testdata/valid_peerlist.yaml: -------------------------------------------------------------------------------- 1 | - 1.1.1.1:1 2 | - 2.2.2.2:2 3 | -------------------------------------------------------------------------------- /testdata/valid_url_peerlist.txt: -------------------------------------------------------------------------------- 1 | http://1.1.1.1:1/foo 2 | tchannel://2.2.2.2:2 3 | -------------------------------------------------------------------------------- /testdata/valid_url_peerlist.yaml: -------------------------------------------------------------------------------- 1 | - http://1.1.1.1:1/foo 2 | - tchannel://2.2.2.2:2 3 | -------------------------------------------------------------------------------- /testdata/yamltothrift/binary_data.file: -------------------------------------------------------------------------------- 1 | testdata -------------------------------------------------------------------------------- /testdata/yamltothrift/binary_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "arg": { 3 | "b": { 4 | "file": "../testdata/yamltothrift/binary_data.file" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testdata/yamltothrift/binary_data.out: -------------------------------------------------------------------------------- 1 | result: 2 | b: 3 | - 116 4 | - 101 5 | - 115 6 | - 116 7 | - 100 8 | - 97 9 | - 116 10 | - 97 11 | -------------------------------------------------------------------------------- /testdata/yamltothrift/binary_data.thrift: -------------------------------------------------------------------------------- 1 | struct S { 2 | 1: optional binary b 3 | } 4 | -------------------------------------------------------------------------------- /testdata/yamltothrift/binary_data.yaml: -------------------------------------------------------------------------------- 1 | arg: 2 | b: !!binary dGVzdGRhdGE= -------------------------------------------------------------------------------- /testdata/yamltothrift/complex_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "arg": { 3 | "m": { 4 | "{\"name\": \"p\"}": { 5 | "name": "t" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testdata/yamltothrift/complex_map.out: -------------------------------------------------------------------------------- 1 | result: 2 | m: 3 | '{"name":"p"}': 4 | name: t 5 | -------------------------------------------------------------------------------- /testdata/yamltothrift/complex_map.thrift: -------------------------------------------------------------------------------- 1 | struct T { 2 | 1: optional string name 3 | } 4 | 5 | struct S { 6 | 1: optional map m 7 | } 8 | -------------------------------------------------------------------------------- /testdata/yamltothrift/complex_map.yaml: -------------------------------------------------------------------------------- 1 | arg: 2 | m: 3 | '{"name":"p"}': 4 | name: t 5 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_formats.out: -------------------------------------------------------------------------------- 1 | result: 2 | l: 3 | - 16 4 | - 16 5 | - 16 6 | - 16 7 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_formats.thrift: -------------------------------------------------------------------------------- 1 | struct S { 2 | 1: optional list l 3 | } 4 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_formats.yaml: -------------------------------------------------------------------------------- 1 | arg: 2 | l: 3 | - 16 4 | # 16 in hex 5 | - 0x10 6 | # 16 in octal 7 | - 020 8 | # 16 in binary 9 | - 0b1_0000 10 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "arg": { 3 | "m": { 4 | "1": 2, 5 | "2": 4 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_map.out: -------------------------------------------------------------------------------- 1 | result: 2 | m: 3 | "1": 2 4 | "2": 4 5 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_map.thrift: -------------------------------------------------------------------------------- 1 | struct S { 2 | 1: optional map m 3 | } 4 | -------------------------------------------------------------------------------- /testdata/yamltothrift/int_map.yaml: -------------------------------------------------------------------------------- 1 | arg: 2 | m: 3 | 1: 2 4 | 2: 4 5 | -------------------------------------------------------------------------------- /testdata/yarpc/integration/fooclient/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by thriftrw-plugin-yarpc 2 | // @generated 3 | 4 | package fooclient 5 | 6 | import ( 7 | "context" 8 | "go.uber.org/thriftrw/wire" 9 | "go.uber.org/yarpc/api/transport" 10 | "go.uber.org/yarpc/encoding/thrift" 11 | "go.uber.org/yarpc" 12 | "github.com/yarpc/yab/testdata/yarpc/integration" 13 | ) 14 | 15 | // Interface is a client for the Foo service. 16 | type Interface interface { 17 | Bar( 18 | ctx context.Context, 19 | Arg *int32, 20 | opts ...yarpc.CallOption, 21 | ) (int32, error) 22 | } 23 | 24 | // New builds a new client for the Foo service. 25 | // 26 | // client := fooclient.New(dispatcher.ClientConfig("foo")) 27 | func New(c transport.ClientConfig, opts ...thrift.ClientOption) Interface { 28 | return client{ 29 | c: thrift.New(thrift.Config{ 30 | Service: "Foo", 31 | ClientConfig: c, 32 | }, opts...), 33 | } 34 | } 35 | 36 | func init() { 37 | yarpc.RegisterClientBuilder(func(c transport.ClientConfig) Interface { 38 | return New(c) 39 | }) 40 | } 41 | 42 | type client struct { 43 | c thrift.Client 44 | } 45 | 46 | func (c client) Bar( 47 | ctx context.Context, 48 | _Arg *int32, 49 | opts ...yarpc.CallOption, 50 | ) (success int32, err error) { 51 | 52 | args := integration.Foo_Bar_Helper.Args(_Arg) 53 | 54 | var body wire.Value 55 | body, err = c.c.Call(ctx, args, opts...) 56 | if err != nil { 57 | return 58 | } 59 | 60 | var result integration.Foo_Bar_Result 61 | if err = result.FromWire(body); err != nil { 62 | return 63 | } 64 | 65 | success, err = integration.Foo_Bar_Helper.UnwrapResponse(&result) 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /testdata/yarpc/integration/fooserver/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by thriftrw-plugin-yarpc 2 | // @generated 3 | 4 | package fooserver 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/yarpc/yab/testdata/yarpc/integration" 10 | "go.uber.org/thriftrw/wire" 11 | "go.uber.org/yarpc/api/transport" 12 | "go.uber.org/yarpc/encoding/thrift" 13 | ) 14 | 15 | // Interface is the server-side interface for the Foo service. 16 | type Interface interface { 17 | Bar( 18 | ctx context.Context, 19 | Arg *int32, 20 | ) (int32, error) 21 | } 22 | 23 | // New prepares an implementation of the Foo service for 24 | // registration. 25 | // 26 | // handler := FooHandler{} 27 | // dispatcher.Register(fooserver.New(handler)) 28 | func New(impl Interface, opts ...thrift.RegisterOption) []transport.Procedure { 29 | h := handler{impl} 30 | service := thrift.Service{ 31 | Name: "Foo", 32 | Methods: []thrift.Method{ 33 | 34 | thrift.Method{ 35 | Name: "bar", 36 | HandlerSpec: thrift.HandlerSpec{ 37 | 38 | Type: transport.Unary, 39 | Unary: thrift.UnaryHandler(h.Bar), 40 | }, 41 | Signature: "Bar(Arg *int32) (int32)", 42 | }, 43 | }, 44 | } 45 | 46 | procedures := make([]transport.Procedure, 0, 1) 47 | procedures = append(procedures, thrift.BuildProcedures(service, opts...)...) 48 | return procedures 49 | } 50 | 51 | type handler struct{ impl Interface } 52 | 53 | func (h handler) Bar(ctx context.Context, body wire.Value) (thrift.Response, error) { 54 | var args integration.Foo_Bar_Args 55 | if err := args.FromWire(body); err != nil { 56 | return thrift.Response{}, err 57 | } 58 | 59 | success, err := h.impl.Bar(ctx, args.Arg) 60 | 61 | hadError := err != nil 62 | result, err := integration.Foo_Bar_Helper.WrapResponse(success, err) 63 | 64 | var response thrift.Response 65 | if err == nil { 66 | response.IsApplicationError = hadError 67 | response.Body = result 68 | } 69 | return response, err 70 | } 71 | -------------------------------------------------------------------------------- /testdata/yarpc/integration/footest/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by thriftrw-plugin-yarpc 2 | // @generated 3 | 4 | package footest 5 | 6 | import ( 7 | "context" 8 | "go.uber.org/yarpc" 9 | "github.com/golang/mock/gomock" 10 | "github.com/yarpc/yab/testdata/yarpc/integration/fooclient" 11 | ) 12 | 13 | // MockClient implements a gomock-compatible mock client for service 14 | // Foo. 15 | type MockClient struct { 16 | ctrl *gomock.Controller 17 | recorder *_MockClientRecorder 18 | } 19 | 20 | var _ fooclient.Interface = (*MockClient)(nil) 21 | 22 | type _MockClientRecorder struct { 23 | mock *MockClient 24 | } 25 | 26 | // Build a new mock client for service Foo. 27 | // 28 | // mockCtrl := gomock.NewController(t) 29 | // client := footest.NewMockClient(mockCtrl) 30 | // 31 | // Use EXPECT() to set expectations on the mock. 32 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 33 | mock := &MockClient{ctrl: ctrl} 34 | mock.recorder = &_MockClientRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows you to define an expectation on the 39 | // Foo mock client. 40 | func (m *MockClient) EXPECT() *_MockClientRecorder { 41 | return m.recorder 42 | } 43 | 44 | // Bar responds to a Bar call based on the mock expectations. This 45 | // call will fail if the mock does not expect this call. Use EXPECT to expect 46 | // a call to this function. 47 | // 48 | // client.EXPECT().Bar(gomock.Any(), ...).Return(...) 49 | // ... := client.Bar(...) 50 | func (m *MockClient) Bar( 51 | ctx context.Context, 52 | _Arg *int32, 53 | opts ...yarpc.CallOption, 54 | ) (success int32, err error) { 55 | 56 | args := []interface{}{ctx, _Arg} 57 | for _, o := range opts { 58 | args = append(args, o) 59 | } 60 | i := 0 61 | ret := m.ctrl.Call(m, "Bar", args...) 62 | success, _ = ret[i].(int32) 63 | i++ 64 | err, _ = ret[i].(error) 65 | return 66 | } 67 | 68 | func (mr *_MockClientRecorder) Bar( 69 | ctx interface{}, 70 | _Arg interface{}, 71 | opts ...interface{}, 72 | ) *gomock.Call { 73 | args := append([]interface{}{ctx, _Arg}, opts...) 74 | return mr.mock.ctrl.RecordCall(mr.mock, "Bar", args...) 75 | } 76 | -------------------------------------------------------------------------------- /testdata/yarpc/integration/types.go: -------------------------------------------------------------------------------- 1 | // Code generated by thriftrw v1.0.0 2 | // @generated 3 | 4 | package integration 5 | 6 | import ( 7 | "fmt" 8 | "go.uber.org/thriftrw/wire" 9 | "strings" 10 | ) 11 | 12 | type NotFound struct{} 13 | 14 | func (v *NotFound) ToWire() (wire.Value, error) { 15 | var ( 16 | fields [0]wire.Field 17 | i int = 0 18 | ) 19 | return wire.NewValueStruct(wire.Struct{Fields: fields[:i]}), nil 20 | } 21 | 22 | func (v *NotFound) FromWire(w wire.Value) error { 23 | for _, field := range w.GetStruct().Fields { 24 | switch field.ID { 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | func (v *NotFound) String() string { 31 | var fields [0]string 32 | i := 0 33 | return fmt.Sprintf("NotFound{%v}", strings.Join(fields[:i], ", ")) 34 | } 35 | 36 | func (v *NotFound) Error() string { 37 | return v.String() 38 | } 39 | -------------------------------------------------------------------------------- /testdata/yarpc/integration/versioncheck.go: -------------------------------------------------------------------------------- 1 | // Code generated by thriftrw v1.0.0 2 | // @generated 3 | 4 | package integration 5 | 6 | import "go.uber.org/thriftrw/version" 7 | 8 | func init() { 9 | version.CheckCompatWithGeneratedCodeAt("1.0.0", "github.com/yarpc/yab/testdata/yarpc/integration") 10 | } 11 | -------------------------------------------------------------------------------- /thrift/const.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package thrift 22 | 23 | import ( 24 | "fmt" 25 | 26 | "go.uber.org/thriftrw/compile" 27 | ) 28 | 29 | func constToRequest(v compile.ConstantValue) interface{} { 30 | switch v := v.(type) { 31 | case compile.ConstantBool: 32 | return bool(v) 33 | case compile.ConstantDouble: 34 | return float64(v) 35 | case compile.ConstantInt: 36 | return int64(v) 37 | case compile.ConstantString: 38 | return string(v) 39 | case compile.ConstReference: 40 | return constToRequest(v.Target.Value) 41 | case compile.EnumItemReference: 42 | return v.Item.Value 43 | case compile.ConstantSet: 44 | return constValueListToRequest([]compile.ConstantValue(v)) 45 | case compile.ConstantList: 46 | return constValueListToRequest([]compile.ConstantValue(v)) 47 | case compile.ConstantMap: 48 | result := make(map[interface{}]interface{}) 49 | for _, value := range v { 50 | // Note: If the key is not hashable, this will fail. 51 | // We should allow using a custom []MapItem to represent a map. 52 | result[constToRequest(value.Key)] = constToRequest(value.Value) 53 | } 54 | return result 55 | case *compile.ConstantStruct: 56 | result := make(map[string]interface{}) 57 | for key, value := range v.Fields { 58 | result[key] = constToRequest(value) 59 | } 60 | return result 61 | default: 62 | panic(fmt.Sprintf("unknown constant type: %T", v)) 63 | } 64 | } 65 | 66 | func constValueListToRequest(v []compile.ConstantValue) interface{} { 67 | result := make([]interface{}, len(v)) 68 | for i, value := range v { 69 | result[i] = constToRequest(value) 70 | } 71 | return result 72 | } 73 | -------------------------------------------------------------------------------- /thrift/const_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package thrift 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "go.uber.org/thriftrw/compile" 28 | ) 29 | 30 | func TestConstToRequest(t *testing.T) { 31 | tests := []struct { 32 | v compile.ConstantValue 33 | want interface{} 34 | }{ 35 | { 36 | v: compile.ConstantBool(true), 37 | want: true, 38 | }, 39 | { 40 | v: compile.ConstantBool(false), 41 | want: false, 42 | }, 43 | { 44 | v: compile.ConstantDouble(1.05), 45 | want: 1.05, 46 | }, 47 | { 48 | v: compile.ConstantInt(1), 49 | want: int64(1), 50 | }, 51 | { 52 | v: compile.ConstantString("foo"), 53 | want: "foo", 54 | }, 55 | { 56 | v: compile.ConstReference{ 57 | Target: &compile.Constant{ 58 | Name: "a", 59 | Type: &compile.StringSpec{}, 60 | Value: compile.ConstantString("foo"), 61 | }, 62 | }, 63 | want: "foo", 64 | }, 65 | { 66 | v: compile.EnumItemReference{ 67 | Item: &compile.EnumItem{ 68 | Name: "a", 69 | Value: 5, 70 | }, 71 | }, 72 | want: int32(5), 73 | }, 74 | { 75 | v: compile.ConstantList{ 76 | compile.ConstantInt(1), 77 | compile.ConstantBool(true), 78 | compile.ConstantString("foo"), 79 | }, 80 | want: []interface{}{int64(1), true, "foo"}, 81 | }, 82 | { 83 | v: compile.ConstantSet{ 84 | compile.ConstantString("foo"), 85 | compile.ConstantSet{compile.ConstantString("bar")}, 86 | }, 87 | want: []interface{}{"foo", []interface{}{"bar"}}, 88 | }, 89 | { 90 | v: compile.ConstantMap{ 91 | {Key: compile.ConstantInt(1), Value: compile.ConstantString("v1")}, 92 | {Key: compile.ConstantBool(true), Value: compile.ConstantString("v2")}, 93 | }, 94 | want: map[interface{}]interface{}{ 95 | int64(1): "v1", 96 | true: "v2", 97 | }, 98 | }, 99 | } 100 | 101 | for _, tt := range tests { 102 | got := constToRequest(tt.v) 103 | assert.Equal(t, tt.want, got, "Result mismatch for %v", tt.v) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /thrift/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package thrift 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "go.uber.org/thriftrw/wire" 28 | ) 29 | 30 | type fieldGroupError struct { 31 | available []string 32 | missingRequired []string 33 | notFound []string 34 | } 35 | 36 | func (e *fieldGroupError) addNotFound(arg string) { 37 | e.notFound = append(e.notFound, arg) 38 | } 39 | 40 | func (e *fieldGroupError) addMissingRequired(arg string) { 41 | e.missingRequired = append(e.missingRequired, arg) 42 | } 43 | 44 | func (e fieldGroupError) Error() string { 45 | messages := []string{"failed to parse fields"} 46 | if len(e.missingRequired) > 0 { 47 | messages = append(messages, 48 | messageList("the following fields are required but not specified", e.missingRequired)) 49 | } 50 | 51 | if len(e.notFound) > 0 { 52 | messages = append(messages, 53 | messageList("the following fields were specified but not found", e.notFound)) 54 | messages = append(messages, 55 | messageList("the available fields are", e.available)) 56 | } 57 | 58 | return strings.Join(messages, "\n") 59 | } 60 | 61 | func (e fieldGroupError) asError() error { 62 | if len(e.missingRequired) == 0 && len(e.notFound) == 0 { 63 | return nil 64 | } 65 | 66 | return e 67 | } 68 | 69 | type specTypeMismatch struct { 70 | specified wire.Type 71 | got wire.Type 72 | } 73 | 74 | func (e specTypeMismatch) Error() string { 75 | return fmt.Sprintf("type specified in Thrift field as %v, got %v", e.specified, e.got) 76 | } 77 | 78 | type specValueMismatch struct { 79 | specName string 80 | underlying error 81 | } 82 | 83 | func (e specValueMismatch) Error() string { 84 | return fmt.Sprintf("field %q failed: %v", e.specName, e.underlying) 85 | } 86 | 87 | type specListItemMismatch struct { 88 | index int 89 | underlying error 90 | } 91 | 92 | func (e specListItemMismatch) Error() string { 93 | return fmt.Sprintf("item %v failed: %v", e.index, e.underlying) 94 | } 95 | 96 | type specMapItemMismatch struct { 97 | specType string 98 | underlying error 99 | } 100 | 101 | func (e specMapItemMismatch) Error() string { 102 | return fmt.Sprintf("%v failed: %v", e.specType, e.underlying) 103 | } 104 | 105 | type specStructFieldMismatch struct { 106 | fieldName string 107 | underlying error 108 | } 109 | 110 | func (e specStructFieldMismatch) Error() string { 111 | return fmt.Sprintf("%q failed: %v", e.fieldName, e.underlying) 112 | } 113 | 114 | // messageList formats a message and a list for an error message. 115 | func messageList(message string, list []string) string { 116 | if len(list) == 0 { 117 | return "" 118 | } 119 | 120 | newList := make([]string, len(list)+1) 121 | newList[0] = message 122 | copy(newList[1:], list) 123 | return strings.Join(newList, "\n\t") 124 | } 125 | -------------------------------------------------------------------------------- /thrift/fields_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package thrift 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestFuzz(t *testing.T) { 30 | tests := []struct { 31 | input, want string 32 | }{ 33 | {"arg", "arg"}, 34 | {"Arg123", "arg123"}, 35 | {"_Arg_1_3", "arg13"}, 36 | } 37 | 38 | for _, tt := range tests { 39 | got := fuzz(tt.input) 40 | assert.Equal(t, tt.want, got, "Fuzz(%v)", tt.input) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /thrift/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package thrift 22 | 23 | // Options controls the serialization of the Thrift request/response. 24 | type Options struct { 25 | UseEnvelopes bool 26 | EnvelopeMethodPrefix string 27 | } 28 | -------------------------------------------------------------------------------- /thrift/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Package thrift contains functionality for converting generic data structures 22 | // to and from Thrift payloads. 23 | package thrift 24 | 25 | import ( 26 | "bytes" 27 | "fmt" 28 | "strings" 29 | 30 | "go.uber.org/thriftrw/compile" 31 | "go.uber.org/thriftrw/protocol" 32 | "go.uber.org/thriftrw/wire" 33 | ) 34 | 35 | // Parse parses the given Thrift file. 36 | func Parse(file string) (*compile.Module, error) { 37 | module, err := compile.Compile(file, compile.NonStrict()) 38 | // thriftrw wraps errors, so we can't use os.IsNotExist here. 39 | if err != nil { 40 | // The user may have left off the ".thrift", so try appending .thrift 41 | if appendedModule, err2 := compile.Compile(file+".thrift", compile.NonStrict()); err2 == nil { 42 | module = appendedModule 43 | err = nil 44 | } 45 | } 46 | return module, err 47 | } 48 | 49 | // SplitMethod takes a method name like Service::Method and splits it 50 | // into Service and Method. 51 | func SplitMethod(fullMethod string) (svc, method string, err error) { 52 | parts := strings.Split(fullMethod, "::") 53 | switch len(parts) { 54 | case 1: 55 | return parts[0], "", nil 56 | case 2: 57 | return parts[0], parts[1], nil 58 | default: 59 | return "", "", fmt.Errorf("invalid Thrift method %q, expected Service::Method", fullMethod) 60 | } 61 | } 62 | 63 | // RequestToBytes takes a user request and converts it to the Thrift binary payload. 64 | // It uses the method spec to convert the user request. 65 | func RequestToBytes(method *compile.FunctionSpec, request map[string]interface{}, opts Options) ([]byte, error) { 66 | w, err := structToValue(compile.FieldGroup(method.ArgsSpec), request) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | buf := &bytes.Buffer{} 72 | if opts.UseEnvelopes { 73 | // Sequence IDs are unused, so use the default, 0. 74 | enveloped := wire.Envelope{ 75 | Name: opts.EnvelopeMethodPrefix + method.Name, 76 | Type: wire.Call, 77 | Value: wire.NewValueStruct(w), 78 | } 79 | err = protocol.Binary.EncodeEnveloped(enveloped, buf) 80 | } else { 81 | err = protocol.Binary.Encode(wire.NewValueStruct(w), buf) 82 | } 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to convert Thrift value to bytes: %v", err) 85 | } 86 | 87 | return buf.Bytes(), nil 88 | } 89 | -------------------------------------------------------------------------------- /transport/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package transport 22 | 23 | import ( 24 | "io" 25 | "time" 26 | 27 | "github.com/opentracing/opentracing-go" 28 | "go.uber.org/yarpc/api/transport" 29 | "golang.org/x/net/context" 30 | ) 31 | 32 | // Request is the fields used to make an RPC. 33 | type Request struct { 34 | TargetService string 35 | Method string 36 | Timeout time.Duration 37 | Headers map[string]string 38 | Baggage map[string]string 39 | TransportHeaders map[string]string 40 | ShardKey string 41 | Body []byte 42 | } 43 | 44 | // StreamRequest is a wrapper of Request, to be used for streaming RPC 45 | type StreamRequest struct { 46 | Request *Request 47 | } 48 | 49 | // Response represents the result of an RPC. 50 | type Response struct { 51 | Headers map[string]string 52 | Body []byte 53 | 54 | // TransportFields contains fields that are transport-specific. 55 | TransportFields map[string]interface{} 56 | } 57 | 58 | // Protocol represents the wire protocol used to send the request. 59 | type Protocol int 60 | 61 | // The list of protocols supported by YAB. 62 | const ( 63 | Unknown Protocol = iota 64 | TChannel 65 | HTTP 66 | GRPC 67 | ) 68 | 69 | // Transport defines the interface for the underlying transport over which 70 | // unary calls are made. 71 | type Transport interface { 72 | Call(ctx context.Context, request *Request) (*Response, error) 73 | Protocol() Protocol 74 | Tracer() opentracing.Tracer 75 | } 76 | 77 | // StreamTransport defines the interface for the underlying transport which 78 | // supports streaming 79 | type StreamTransport interface { 80 | CallStream(ctx context.Context, request *StreamRequest) (*transport.ClientStream, error) 81 | } 82 | 83 | // TransportCloser is a Transport that can be closed. 84 | type TransportCloser interface { 85 | Transport 86 | io.Closer 87 | } 88 | -------------------------------------------------------------------------------- /transport/request_interceptor.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "context" 4 | 5 | // stores the currently registered middleware 6 | var registeredInterceptor RequestInterceptor 7 | 8 | // RegisterInterceptor sets the provided request interceptor to be used on future 9 | // calls to ApplyInterceptor(). Calls to RegisterInterceptor() will overwrite previously 10 | // registered interceptors; that is, only one interceptor is allowed at a time. 11 | // Returns a function to undo the change made by this call. 12 | func RegisterInterceptor(newRI RequestInterceptor) (restore func()) { 13 | oldRI := registeredInterceptor 14 | registeredInterceptor = newRI 15 | return func() { 16 | registeredInterceptor = oldRI 17 | } 18 | } 19 | 20 | // RequestInterceptor allows for its implementors to modify a pre-flight Request. 21 | type RequestInterceptor interface { 22 | // Apply mutates and returns the passed Request object. 23 | Apply(ctx context.Context, req *Request) (*Request, error) 24 | } 25 | 26 | // ApplyInterceptor mutates a Request using the previously registered RequestInterceptor. 27 | func ApplyInterceptor(ctx context.Context, req *Request) (*Request, error) { 28 | if registeredInterceptor == nil { 29 | return req, nil 30 | } 31 | return registeredInterceptor.Apply(ctx, req) 32 | } 33 | -------------------------------------------------------------------------------- /transport/request_interceptor_test.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type headerRequestInterceptor struct { 13 | wantErr bool 14 | } 15 | 16 | func (ri headerRequestInterceptor) Apply(ctx context.Context, req *Request) (*Request, error) { 17 | req.Headers["foo"] = "bar" 18 | if ri.wantErr { 19 | return nil, errors.New("bad apply") 20 | } 21 | return req, nil 22 | } 23 | 24 | func TestRequestInterceptor(t *testing.T) { 25 | tests := []struct { 26 | dontRegister bool 27 | wantErr bool 28 | }{ 29 | { /* run without options */ }, 30 | {wantErr: true}, 31 | {dontRegister: true}, 32 | } 33 | for idx, tt := range tests { 34 | restore := func() {} 35 | 36 | // create the test interceptor 37 | ri := &headerRequestInterceptor{ 38 | wantErr: tt.wantErr, 39 | } 40 | if !tt.dontRegister { 41 | restore = RegisterInterceptor(ri) 42 | require.Equal(t, ri, registeredInterceptor) 43 | } 44 | 45 | // create test request 46 | headers := map[string]string{"zim": "zam"} 47 | rawReq := &Request{ 48 | Headers: headers, 49 | Method: "get", 50 | } 51 | 52 | // modify the test request 53 | req, err := ApplyInterceptor(context.Background(), rawReq) 54 | restore() 55 | if tt.dontRegister { 56 | assert.NoError(t, err, "[%d] apply should not error", idx) 57 | _, ok := req.Headers["foo"] 58 | assert.False(t, ok, "[%d] test interceptor should not have applied", idx) 59 | continue 60 | } 61 | if tt.wantErr { 62 | assert.Error(t, err) 63 | continue 64 | } 65 | 66 | // verify previous values 67 | for k, v := range headers { 68 | assert.Equal(t, v, req.Headers[k], "[%d] previous header was not preserved", idx) 69 | } 70 | assert.Equal(t, "get", req.Method, "[%d] previous method was not preserved", idx) 71 | 72 | // verify modified values 73 | assert.Equal(t, "bar", req.Headers["foo"], "[%d] test interceptor should have applied", idx) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /unmarshal/unmarshal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package unmarshal 22 | 23 | import ( 24 | "bytes" 25 | "encoding/json" 26 | "fmt" 27 | 28 | "gopkg.in/yaml.v2" 29 | ) 30 | 31 | // YAML unmarshals the given YAML input to a map. 32 | func YAML(bs []byte) (map[string]interface{}, error) { 33 | var m map[string]interface{} 34 | if err := yaml.Unmarshal(bs, &m); err != nil { 35 | return nil, err 36 | } 37 | 38 | return m, nil 39 | } 40 | 41 | // JSON unmarshals the given JSON input to an interface{}. 42 | func JSON(bs []byte) (interface{}, error) { 43 | // An empty body should produce an empty input map. 44 | if len(bs) == 0 { 45 | return nil, nil 46 | } 47 | 48 | decoder := json.NewDecoder(bytes.NewReader(bs)) 49 | decoder.UseNumber() 50 | 51 | var data interface{} 52 | if err := decoder.Decode(&data); err != nil { 53 | return nil, fmt.Errorf("failed to parse JSON: %v", err) 54 | } 55 | 56 | return data, nil 57 | } 58 | -------------------------------------------------------------------------------- /unmarshal/unmarshal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package unmarshal 22 | 23 | import ( 24 | "encoding/json" 25 | "io/ioutil" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func mustRead(fname string) []byte { 32 | bs, err := ioutil.ReadFile(fname) 33 | if err != nil { 34 | panic(err) 35 | } 36 | return bs 37 | } 38 | 39 | func TestJSON(t *testing.T) { 40 | tests := []struct { 41 | input []byte 42 | want interface{} 43 | wantErr bool 44 | }{ 45 | { 46 | input: nil, 47 | want: nil, 48 | }, 49 | { 50 | input: []byte("{}"), 51 | want: map[string]interface{}{}, 52 | }, 53 | { 54 | input: mustRead("../testdata/valid.json"), 55 | want: map[string]interface{}{ 56 | "k1": "v1", 57 | "k2": json.Number("5"), 58 | }, 59 | }, 60 | { 61 | input: []byte("{"), 62 | wantErr: true, 63 | }, 64 | { 65 | input: []byte(`"hello"`), 66 | want: "hello", 67 | }, 68 | { 69 | input: []byte(`true`), 70 | want: true, 71 | }, 72 | } 73 | 74 | for _, tt := range tests { 75 | got, err := JSON(tt.input) 76 | if tt.wantErr { 77 | assert.Error(t, err, "unmarshalJSON(%s) should fail", tt.input) 78 | continue 79 | } 80 | 81 | if assert.NoError(t, err, "unmarshalJSON(%s) should not fail", tt.input) { 82 | assert.Equal(t, tt.want, got, "unmarshalJSON(%s) unexpected result", tt.input) 83 | } 84 | } 85 | } 86 | 87 | func TestYAML(t *testing.T) { 88 | tests := []struct { 89 | msg string 90 | input string 91 | jsonInput string 92 | want map[string]interface{} 93 | wantErr bool 94 | }{ 95 | { 96 | msg: "basic types", 97 | input: ` 98 | str: v1 99 | int: 5 100 | bool: true 101 | int64: 9223372036854775807 102 | uint64: 18446744073709551615 103 | float: 6.5`, 104 | want: map[string]interface{}{ 105 | "str": "v1", 106 | "int": 5, 107 | "bool": true, 108 | "int64": 9223372036854775807, 109 | "uint64": uint64(18446744073709551615), 110 | "float": 6.5, 111 | }, 112 | }, 113 | { 114 | msg: "inline objects", 115 | input: ` 116 | obj: 117 | 1: 2 118 | 3: 5.6`, 119 | want: map[string]interface{}{ 120 | "obj": map[interface{}]interface{}{ 121 | 1: 2, 122 | 3: 5.6, 123 | }, 124 | }, 125 | }, 126 | { 127 | msg: "json is yaml", 128 | input: `{ 129 | "str": "v1", 130 | "int": 5, 131 | "bool": true, 132 | "int64": 9223372036854775807, 133 | "uint64": 18446744073709551615, 134 | "float": 6.5, 135 | "obj": {"k1": "v1"} 136 | }`, 137 | want: map[string]interface{}{ 138 | "str": "v1", 139 | "int": 5, 140 | "bool": true, 141 | "int64": 9223372036854775807, 142 | "uint64": uint64(18446744073709551615), 143 | "float": 6.5, 144 | "obj": map[interface{}]interface{}{ 145 | "k1": "v1", 146 | }, 147 | }, 148 | }, 149 | { 150 | msg: "json with trailing comma", 151 | input: `{ 152 | "str": "v1", 153 | }`, 154 | want: map[string]interface{}{ 155 | "str": "v1", 156 | }, 157 | }, 158 | { 159 | msg: "json with map[int]int", 160 | input: `{ 161 | "obj": { 162 | 1: 2, 163 | 3: 4, 164 | } 165 | }`, 166 | want: map[string]interface{}{ 167 | "obj": map[interface{}]interface{}{ 168 | 1: 2, 169 | 3: 4, 170 | }, 171 | }, 172 | }, 173 | { 174 | msg: "invalid yaml", 175 | input: `{"str" "asd"}`, 176 | wantErr: true, 177 | }, 178 | } 179 | 180 | for _, tt := range tests { 181 | got, err := YAML([]byte(tt.input)) 182 | if tt.wantErr { 183 | assert.Error(t, err, "%v: should fail", tt.msg) 184 | continue 185 | } 186 | if !assert.NoError(t, err, "%v: should succeed", tt.msg) { 187 | continue 188 | } 189 | assert.Equal(t, tt.want, got, "%v: mismatch", tt.msg) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /utils_for_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "io" 27 | "io/ioutil" 28 | "runtime" 29 | "sync" 30 | "testing" 31 | 32 | "github.com/opentracing/opentracing-go" 33 | "github.com/stretchr/testify/require" 34 | "github.com/uber/jaeger-client-go" 35 | "github.com/yarpc/yab/encoding" 36 | "github.com/yarpc/yab/transport" 37 | "go.uber.org/zap" 38 | ) 39 | 40 | // Constants useful for tests 41 | const ( 42 | validThrift = "testdata/simple.thrift" 43 | fooMethod = "Simple::foo" 44 | exampleTemplate = "testdata/templates/foo.yab" 45 | ) 46 | 47 | var _testLogger = zap.NewNop() 48 | 49 | var ( 50 | _resolvedTChannelThrift = resolvedProtocolEncoding{protocol: transport.TChannel, enc: encoding.Thrift} 51 | _resolvedTChannelRaw = resolvedProtocolEncoding{protocol: transport.TChannel, enc: encoding.Raw} 52 | _resolvedGrpcProto = resolvedProtocolEncoding{protocol: transport.GRPC, enc: encoding.Protobuf} 53 | ) 54 | 55 | type testOutput struct { 56 | *bytes.Buffer 57 | warnf func(string, ...interface{}) 58 | fatalf func(string, ...interface{}) 59 | } 60 | 61 | func (t testOutput) Fatalf(format string, args ...interface{}) { 62 | t.fatalf(format, args...) 63 | runtime.Goexit() 64 | } 65 | 66 | func (t testOutput) Printf(format string, args ...interface{}) { 67 | t.WriteString(fmt.Sprintf(format, args...)) 68 | } 69 | 70 | func (t testOutput) Warnf(format string, args ...interface{}) { 71 | t.warnf(format, args...) 72 | } 73 | 74 | func getOutput(t *testing.T) (*bytes.Buffer, *bytes.Buffer, output) { 75 | outBuf := &bytes.Buffer{} 76 | warnBuf := &bytes.Buffer{} 77 | out := testOutput{ 78 | Buffer: outBuf, 79 | warnf: func(format string, args ...interface{}) { 80 | warnBuf.WriteString(fmt.Sprintf(format, args...)) 81 | }, 82 | fatalf: t.Errorf, 83 | } 84 | return outBuf, warnBuf, out 85 | } 86 | 87 | func writeFile(t *testing.T, prefix, contents string) string { 88 | f, err := ioutil.TempFile("", prefix) 89 | require.NoError(t, err, "TempFile failed") 90 | _, err = f.WriteString(contents) 91 | require.NoError(t, err, "Write to temp file failed") 92 | require.NoError(t, f.Close(), "Close temp file failed") 93 | return f.Name() 94 | } 95 | 96 | type debugThrottler struct { 97 | sync.Mutex 98 | credits int 99 | } 100 | 101 | func (d *debugThrottler) IsAllowed(operation string) bool { 102 | const creditsPerDebugTrace = 1 103 | 104 | d.Lock() 105 | defer d.Unlock() 106 | 107 | if d.credits >= creditsPerDebugTrace { 108 | d.credits -= creditsPerDebugTrace 109 | return true 110 | } 111 | return false 112 | } 113 | 114 | func newDebugThrottler(credits int) *debugThrottler { 115 | return &debugThrottler{credits: credits} 116 | } 117 | 118 | type debugTraceCounter struct { 119 | sync.Mutex 120 | numDebugSpans int 121 | max int 122 | t *testing.T 123 | } 124 | 125 | func newDebugTraceCounter(t *testing.T, numDebugTraces int) jaeger.Reporter { 126 | return &debugTraceCounter{numDebugSpans: 0, max: numDebugTraces, t: t} 127 | } 128 | 129 | func (d *debugTraceCounter) Report(span *jaeger.Span) { 130 | d.Lock() 131 | defer d.Unlock() 132 | 133 | if span.Context().(jaeger.SpanContext).IsDebug() { 134 | d.numDebugSpans += 1 135 | } 136 | } 137 | 138 | func (d *debugTraceCounter) Close() { 139 | d.Lock() 140 | defer d.Unlock() 141 | 142 | require.True(d.t, d.numDebugSpans <= d.max, "Incorrect number of debug traces") 143 | } 144 | 145 | func getTestTracer(t *testing.T, serviceName string) (opentracing.Tracer, io.Closer) { 146 | const credits = 10 147 | return getTestTracerWithCredits(t, serviceName, credits) 148 | } 149 | 150 | func getTestTracerWithCredits(t *testing.T, serviceName string, credits int) (opentracing.Tracer, io.Closer) { 151 | return jaeger.NewTracer( 152 | serviceName, 153 | jaeger.NewConstSampler(true), 154 | newDebugTraceCounter(t, credits), 155 | jaeger.TracerOptions.DebugThrottler(newDebugThrottler(credits)), 156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | // versionString is the sem-ver version string for yab. 24 | // It will be bumped explicitly on releases. 25 | var versionString = "0.24.0" 26 | --------------------------------------------------------------------------------