├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── circuit.go ├── go.mod ├── go.sum ├── internal └── circuitgentest │ ├── aggregator.gen.go │ ├── aggregator.go │ ├── aggregator_test.go │ ├── circuittest │ ├── aggregator.gen.go │ ├── gen.go │ ├── gen_test.go │ ├── publisher.gen.go │ └── pubsub.gen.go │ ├── gen.go │ ├── model │ └── model.go │ ├── publisher.gen.go │ ├── publisher.go │ ├── publishercircuitv3.gen.go │ └── rep │ └── rep.go ├── license_test.go ├── main.go ├── parsing.go ├── template_models.go └── tools.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | shell: 'sh -x' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false # report all go versions separately 17 | matrix: 18 | go-version: 19 | - "1.12" 20 | - "1.13" 21 | - "1.14" 22 | - "1.15" 23 | - "1.16" 24 | - "1.17" 25 | - "1.18" 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Set up Go 30 | uses: actions/setup-go@v3 31 | with: 32 | go-version: ${{matrix.go-version}} 33 | 34 | - name: Build 35 | run: make test 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /circuitgen 2 | /bin 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export SHELL := /bin/bash 2 | export PATH := $(PWD)/bin:$(PWD):$(PATH) 3 | export GOBIN := $(PWD)/bin 4 | 5 | default: test 6 | 7 | clean: 8 | @find internal -name "*.gen.go" -exec rm {} \; 9 | .PHONY: clean 10 | 11 | generate: clean 12 | go build 13 | go generate ./... 14 | .PHONY: generate 15 | 16 | test: lint generate 17 | go test -race ./... 18 | .PHONY: test 19 | 20 | install-tools: 21 | @mkdir -p bin 22 | go install github.com/kisielk/errcheck 23 | go install golang.org/x/lint/golint 24 | go install golang.org/x/tools/cmd/goimports 25 | go install github.com/securego/gosec/cmd/gosec 26 | .PHONY: install-tools 27 | 28 | lint: install-tools 29 | go vet ./... 30 | errcheck -asserts -blank ./... 31 | golint -set_exit_status ./... 32 | gosec -quiet ./... 33 | .PHONY: lint 34 | 35 | fix: install-tools 36 | go fmt ./... 37 | find . -iname "*.go" -print0 | xargs -0 goimports -w 38 | .PHONY: fix 39 | 40 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | circuitgen 2 | Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circuitgen 2 | 3 | circuitgen generates a circuit wrapper around an interface or struct that encapsulates calling [circuits](https://github.com/cep21/circuit). 4 | A wrapper struct matching the interface or struct method set is generated, and each method call that is context-aware and returning an error is wrapped 5 | by a circuit. These wrapper structs have no outside Go dependencies besides the interface or struct's dependencies. 6 | 7 | It's important to provide a `IsBadRequest` to [not count user errors](https://github.com/cep21/circuit#not-counting-user-error-as-a-fault) against the circuit. A bad request is not counted as a success or failure in the circuit, so it does not affect opening or closing the circuit. 8 | For example, a spike in HTTP 4xx errors (ex. Validation errors) should not open the circuit. 9 | 10 | An optional `ShouldSkipError` can be provided so that the call is counted as successful even if there is a non-nil error. 11 | For example, DynamoDB responses that return ConditionalCheckedFailException (CCFE) should be counted as successful requests. In the scenario where CCFE is counted as a bad request, if the client is getting CCFE a majority of the time, and the circuit opens (ex. spike of timeouts), then the circuit will prolong closing the circuit until the circuit happens to make a request that doesn't return CCFE. 12 | 13 | ## Method Wrapping Requirements 14 | 15 | When deciding if making a circuit wrapper is right for your interface or struct, consider that methods will only be wrapped if: 16 | * The method accepts a context as the first argument 17 | * The method returns an error as the last value 18 | 19 | Example 20 | ```go 21 | type Publisher interface { 22 | // Method is wrapped 23 | Publish(ctx context.Context, message string) error 24 | // Method is *not* wrapped 25 | Close() error 26 | } 27 | ``` 28 | 29 | # Installation 30 | 31 | ```bash 32 | go get github.com/twitchtv/circuitgen 33 | ``` 34 | 35 | # Usage 36 | 37 | ```bash 38 | circuitgen --pkg --name --out [--alias ] [--circuit-major-version ] 39 | ``` 40 | 41 | Add `./vendor/` to package path if the dependency is vendored; when using Go modules this is unnecessary. 42 | 43 | Set the `circuit-major-version` flag if using Go modules and major version 3 or later. This makes the wrappers import the same version as the rest of your code. 44 | 45 | ## Example 46 | 47 | Generating the DynamoDB client into the wrappers directory with circuits aliased as "DynamoDB" 48 | 49 | ```bash 50 | circuitgen --pkg github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface --name DynamoDBAPI --alias DynamoDB --out internal/wrappers --circuit-major-version 3 51 | ``` 52 | 53 | This generates a circuit wrapper that satifies the `github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface.DynamoDBAPI` interface. 54 | 55 | ```go 56 | // Code generated by circuitgen tool. DO NOT EDIT 57 | 58 | package wrappers 59 | 60 | import ( 61 | "context" 62 | 63 | "github.com/aws/aws-sdk-go/aws/request" 64 | "github.com/aws/aws-sdk-go/service/dynamodb" 65 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" 66 | "github.com/cep21/circuit/v3" 67 | ) 68 | 69 | // CircuitWrapperDynamoDBConfig contains configuration for CircuitWrapperDynamoDB. All fields are optional 70 | type CircuitWrapperDynamoDBConfig struct { 71 | // ShouldSkipError determines whether an error should be skipped and have the circuit 72 | // track the call as successful. This takes precedence over IsBadRequest 73 | ShouldSkipError func(error) bool 74 | 75 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 76 | IsBadRequest func(error) bool 77 | 78 | // Prefix is prepended to all circuit names 79 | Prefix string 80 | 81 | // Defaults are used for all created circuits. Per-circuit configs override this 82 | Defaults circuit.Config 83 | 84 | // CircuitBatchGetItemPagesWithContext is the configuration used for the BatchGetItemPagesWithContext circuit. This overrides values set by Defaults 85 | CircuitBatchGetItemPagesWithContext circuit.Config 86 | // CircuitBatchGetItemWithContext is the configuration used for the BatchGetItemWithContext circuit. This overrides values set by Defaults 87 | CircuitBatchGetItemWithContext circuit.Config 88 | 89 | // ... Rest omitted 90 | } 91 | 92 | // CircuitWrapperDynamoDB is a circuit wrapper for dynamodbiface.DynamoDBAPI 93 | type CircuitWrapperDynamoDB struct { 94 | dynamodbiface.DynamoDBAPI 95 | 96 | // ShouldSkipError determines whether an error should be skipped and have the circuit 97 | // track the call as successful. This takes precedence over IsBadRequest 98 | ShouldSkipError func(error) bool 99 | 100 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 101 | IsBadRequest func(error) bool 102 | 103 | // CircuitBatchGetItemPagesWithContext is the circuit for method BatchGetItemPagesWithContext 104 | CircuitBatchGetItemPagesWithContext *circuit.Circuit 105 | // CircuitBatchGetItemWithContext is the circuit for method BatchGetItemWithContext 106 | CircuitBatchGetItemWithContext *circuit.Circuit 107 | 108 | // ... Rest omitted 109 | } 110 | 111 | // NewCircuitWrapperDynamoDB creates a new circuit wrapper and initializes circuits 112 | func NewCircuitWrapperDynamoDB( 113 | manager *circuit.Manager, 114 | embedded dynamodbiface.DynamoDBAPI, 115 | conf CircuitWrapperDynamoDBConfig, 116 | ) (*CircuitWrapperDynamoDB, error) { 117 | if conf.ShouldSkipError == nil { 118 | conf.ShouldSkipError = func(err error) bool { 119 | return false 120 | } 121 | } 122 | 123 | if conf.IsBadRequest == nil { 124 | conf.IsBadRequest = func(err error) bool { 125 | return false 126 | } 127 | } 128 | 129 | w := &CircuitWrapperDynamoDB{ 130 | DynamoDBAPI: embedded, 131 | ShouldSkipError: conf.ShouldSkipError, 132 | IsBadRequest: conf.IsBadRequest, 133 | } 134 | 135 | var err error 136 | 137 | w.CircuitBatchGetItemPagesWithContext, err = manager.CreateCircuit(conf.Prefix+"DynamoDB.BatchGetItemPagesWithContext", conf.CircuitBatchGetItemPagesWithContext, conf.Defaults) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | w.CircuitBatchGetItemWithContext, err = manager.CreateCircuit(conf.Prefix+"DynamoDB.BatchGetItemWithContext", conf.CircuitBatchGetItemWithContext, conf.Defaults) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | // ... Rest omitted 148 | 149 | return w, nil 150 | } 151 | 152 | // BatchGetItemPagesWithContext calls the embedded dynamodbiface.DynamoDBAPI's method BatchGetItemPagesWithContext with CircuitBatchGetItemPagesWithContext 153 | func (w *CircuitWrapperDynamoDB) BatchGetItemPagesWithContext(ctx context.Context, p1 *dynamodb.BatchGetItemInput, p2 func(*dynamodb.BatchGetItemOutput, bool) bool, p3 ...request.Option) error { 154 | var skippedErr error 155 | 156 | err := w.CircuitBatchGetItemPagesWithContext.Run(ctx, func(ctx context.Context) error { 157 | err := w.DynamoDBAPI.BatchGetItemPagesWithContext(ctx, p1, p2, p3...) 158 | 159 | if w.ShouldSkipError(err) { 160 | skippedErr = err 161 | return nil 162 | } 163 | 164 | if w.IsBadRequest(err) { 165 | return &circuit.SimpleBadRequest{Err: err} 166 | } 167 | 168 | return err 169 | }) 170 | 171 | if skippedErr != nil { 172 | err = skippedErr 173 | } 174 | 175 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 176 | err = berr.Err 177 | } 178 | 179 | return err 180 | } 181 | 182 | // BatchGetItemWithContext calls the embedded dynamodbiface.DynamoDBAPI's method BatchGetItemWithContext with CircuitBatchGetItemWithContext 183 | func (w *CircuitWrapperDynamoDB) BatchGetItemWithContext(ctx context.Context, p1 *dynamodb.BatchGetItemInput, p2 ...request.Option) (*dynamodb.BatchGetItemOutput, error) { 184 | var r0 *dynamodb.BatchGetItemOutput 185 | var skippedErr error 186 | 187 | err := w.CircuitBatchGetItemWithContext.Run(ctx, func(ctx context.Context) error { 188 | var err error 189 | r0, err = w.DynamoDBAPI.BatchGetItemWithContext(ctx, p1, p2...) 190 | 191 | if w.ShouldSkipError(err) { 192 | skippedErr = err 193 | return nil 194 | } 195 | 196 | if w.IsBadRequest(err) { 197 | return &circuit.SimpleBadRequest{Err: err} 198 | } 199 | 200 | return err 201 | }) 202 | 203 | if skippedErr != nil { 204 | err = skippedErr 205 | } 206 | 207 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 208 | err = berr.Err 209 | } 210 | 211 | return r0, err 212 | } 213 | 214 | // ... Rest of methods omitted 215 | 216 | var _ dynamodbiface.DynamoDBAPI = (*CircuitWrapperDynamoDB)(nil) 217 | ``` 218 | 219 | The wrapper can be used like such 220 | 221 | ```go 222 | func createWrappedClient() (dynamodbiface.DynamoDBAPI, error) { 223 | m := &circuit.Manager{} // Simplest manager 224 | 225 | // Create embedded client 226 | sess := session.Must(session.NewSession(&aws.Config{})) 227 | client := dynamodb.New(sess) 228 | 229 | // Create circuit wrapped client 230 | wrappedClient, err := wrappers.NewCircuitWrapperDynamoDB(m, client, wrappers.CircuitWrapperDynamoDBConfig{ 231 | // Custom check to skip errors to not count against the circuit. For DynamoDB specifically, ConditionalCheckFailedException 232 | // errors are considered successful requests 233 | ShouldSkipError: func(err error) bool { 234 | aerr, ok := err.(awserr.Error) 235 | return ok && aerr.Code() == dynamodb.ErrCodeConditionalCheckFailedException 236 | }, 237 | // Custom check for bad request. This is important to not count user errors as faults. 238 | // See https://github.com/cep21/circuit#not-counting-user-error-as-a-fault 239 | IsBadRequest: func(err error) bool { 240 | rerr, ok := err.(awserr.RequestFailure) 241 | if ok { 242 | return rerr.StatusCode() >= 400 && rerr.StatusCode() < 500 243 | } 244 | return false 245 | }, 246 | // Override defaults for the GetItemWithContext circuit 247 | CircuitGetItemWithContext: circuit.Config{ 248 | Execution: circuit.ExecutionConfig{ 249 | Timeout: 200 * time.Millisecond, // Override default timeout 250 | }, 251 | }, 252 | }) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | return wrappedClient, nil 258 | } 259 | ``` 260 | 261 | # Development 262 | 263 | Go version 1.12 or beyond is recommended for development. 264 | 265 | Run `make test` to run Go tests. 266 | 267 | # License 268 | 269 | This library is licensed under the Apache 2.0 License. 270 | 271 | # Contributing 272 | 273 | Any pull requests are extremely welcome! If you run into problems or have questions, please raise a github issue! 274 | -------------------------------------------------------------------------------- /circuit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "bytes" 18 | "errors" 19 | "fmt" 20 | "go/format" 21 | "io/ioutil" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | "text/template" 26 | "time" 27 | 28 | "github.com/spf13/cobra" 29 | "github.com/spf13/pflag" 30 | "golang.org/x/tools/imports" 31 | ) 32 | 33 | // circuitWrapperTemplate is a template for generating a circuit wrapper. Conditional logic should be minimized in the 34 | // template for readability. 35 | var circuitWrapperTemplate = template.Must(template.New("").Parse(` 36 | // Code ` + `generated by circuitgen tool. DO NOT EDIT 37 | 38 | package {{ .PackageName }} 39 | 40 | import ( 41 | "context" 42 | "github.com/cep21/circuit{{ .VersionSuffix }}" 43 | {{ range .TypeMetadata.Imports -}} 44 | "{{ .Path }}" 45 | {{ end -}} 46 | ) 47 | 48 | // {{ .WrapperStructName }}Config contains configuration for {{ .WrapperStructName }}. All fields are optional 49 | type {{ .WrapperStructName }}Config struct { 50 | // ShouldSkipError determines whether an error should be skipped and have the circuit 51 | // track the call as successful. This takes precedence over IsBadRequest 52 | ShouldSkipError func(error) bool 53 | 54 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 55 | IsBadRequest func(error) bool 56 | 57 | // Prefix is prepended to all circuit names 58 | Prefix string 59 | 60 | // Defaults are used for all created circuits. Per-circuit configs override this 61 | Defaults circuit.Config 62 | 63 | {{ range $i, $meth := .TypeMetadata.Methods -}} 64 | {{ if $meth.IsWrappingSupported -}} 65 | // Circuit{{ $meth.Name }} is the configuration used for the {{ $meth.Name }} circuit. This overrides values set by Defaults 66 | Circuit{{ $meth.Name }} circuit.Config 67 | {{ end -}} 68 | {{ end }} 69 | } 70 | 71 | // {{ .WrapperStructName }} is a circuit wrapper for {{ .EmbeddedType }} 72 | type {{ .WrapperStructName }} struct { 73 | {{ .EmbeddedType }} 74 | 75 | // ShouldSkipError determines whether an error should be skipped and have the circuit 76 | // track the call as successful. This takes precedence over IsBadRequest 77 | ShouldSkipError func(error) bool 78 | 79 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 80 | IsBadRequest func(error) bool 81 | 82 | {{ range $i, $meth := .TypeMetadata.Methods -}} 83 | {{ if $meth.IsWrappingSupported -}} 84 | // Circuit{{ $meth.Name }} is the circuit for method {{ $meth.Name }} 85 | Circuit{{ $meth.Name }} *circuit.Circuit 86 | {{ end -}} 87 | {{ end }} 88 | } 89 | 90 | // New{{ .WrapperStructName }} creates a new circuit wrapper and initializes circuits 91 | func New{{ .WrapperStructName }}( 92 | manager *circuit.Manager, 93 | embedded {{ .EmbeddedType }}, 94 | conf {{ .WrapperStructName }}Config, 95 | ) (*{{ .WrapperStructName }}, error) { 96 | if conf.ShouldSkipError == nil { 97 | conf.ShouldSkipError = func(err error) bool { 98 | return false 99 | } 100 | } 101 | 102 | if conf.IsBadRequest == nil { 103 | conf.IsBadRequest = func(err error) bool { 104 | return false 105 | } 106 | } 107 | 108 | w := &{{ .WrapperStructName }}{ 109 | {{ .EmbeddedName }}: embedded, 110 | ShouldSkipError: conf.ShouldSkipError, 111 | IsBadRequest: conf.IsBadRequest, 112 | } 113 | 114 | var err error 115 | {{ range $i, $meth := .TypeMetadata.Methods -}} 116 | {{ if $meth.IsWrappingSupported -}} 117 | w.Circuit{{ $meth.Name }}, err = manager.CreateCircuit(conf.Prefix + "{{ $.Alias }}.{{ $meth.Name }}", conf.Circuit{{ $meth.Name}}, conf.Defaults) 118 | if err != nil { 119 | return nil, err 120 | } 121 | {{ end }} 122 | {{ end }} 123 | 124 | return w, nil 125 | } 126 | 127 | {{ range $i, $meth := .TypeMetadata.Methods }} 128 | {{ if $meth.IsWrappingSupported -}} 129 | // {{ $meth.Name }} calls the embedded {{ $.EmbeddedType }}'s method {{ $meth.Name}} with Circuit{{ $meth.Name }} 130 | func (w *{{ $.WrapperStructName }}) {{ $meth.Name }}({{ $meth.ParamsSignature "ctx"}}) {{ $meth.ResultsSignature }} { 131 | {{ $meth.ResultsClosureVariableDeclarations -}} 132 | var skippedErr error 133 | 134 | err := w.Circuit{{ $meth.Name }}.Run(ctx, func(ctx context.Context) error { 135 | {{ if $meth.HasOneMethodResultVariable -}} 136 | err := w.{{ $.EmbeddedName }}.{{ $meth.Name }}({{ $meth.CallSignatureWithClosure }}) 137 | {{ else -}} 138 | var err error 139 | {{ $meth.ResultsCircuitVariableAssignments }} = w.{{ $.EmbeddedName }}.{{ $meth.Name }}({{ $meth.CallSignatureWithClosure }}) 140 | {{ end }} 141 | 142 | if w.ShouldSkipError(err) { 143 | skippedErr = err 144 | return nil 145 | } 146 | 147 | if w.IsBadRequest(err) { 148 | return &circuit.SimpleBadRequest{Err: err} 149 | } 150 | return err 151 | }) 152 | 153 | if skippedErr != nil { 154 | err = skippedErr 155 | } 156 | 157 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 158 | err = berr.Err 159 | } 160 | 161 | return {{ $meth.ResultsClosureVariableReturns }} err 162 | } 163 | {{ end }} 164 | {{ end }} 165 | 166 | {{if .IsInterface -}} 167 | var _ {{ .EmbeddedType }} = (*{{ .WrapperStructName}})(nil) 168 | {{ end }} 169 | `)) 170 | 171 | type circuitWrapperTemplateContext struct { 172 | PackageName string 173 | Alias string 174 | VersionSuffix string 175 | TypeMetadata TypeMetadata 176 | } 177 | 178 | // ex. "dynamodbiface.DynamoDBAPI" 179 | func (t *circuitWrapperTemplateContext) EmbeddedType() string { 180 | if t.IsInterface() { 181 | return t.TypeMetadata.TypeInfo.Name 182 | } 183 | return "*" + t.TypeMetadata.TypeInfo.Name // assume struct with pointer receiver 184 | } 185 | 186 | func (t *circuitWrapperTemplateContext) EmbeddedName() string { 187 | return t.TypeMetadata.TypeInfo.NameWithoutQualifier 188 | } 189 | 190 | func (t *circuitWrapperTemplateContext) WrapperStructName() string { 191 | return "CircuitWrapper" + t.Alias 192 | } 193 | 194 | func (t *circuitWrapperTemplateContext) IsInterface() bool { 195 | return t.TypeMetadata.TypeInfo.IsInterface 196 | } 197 | 198 | type circuitCmd struct { 199 | pkg string 200 | name string 201 | out string 202 | alias string 203 | majorVersion int 204 | debug bool 205 | goimports bool 206 | } 207 | 208 | func (c *circuitCmd) Cobra() *cobra.Command { 209 | cmd := &cobra.Command{ 210 | Use: "circuitgen --pkg --name --out [--alias ]", 211 | Example: "circuitgen --pkg github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface --name DynamoDBAPI --alias DynamoDB --out internal/wrappers", 212 | Short: "circuitgen is a circuit wrapper generator for interfaces and structs", 213 | RunE: func(cmd *cobra.Command, args []string) error { 214 | return c.Execute() 215 | }, 216 | DisableFlagsInUseLine: true, 217 | } 218 | 219 | pf := cmd.PersistentFlags() 220 | pf.StringVar(&c.pkg, "pkg", "", "(Required) The path to the package. Add ./vendor if the dependency is vendored") 221 | markFlagRequired(pf, "pkg") 222 | 223 | pf.StringVar(&c.name, "name", "", "(Required) The name of the type (interface or struct) in the package path") 224 | markFlagRequired(pf, "name") 225 | 226 | pf.StringVar(&c.out, "out", "", "(Required) The output path. A default filename is given if the path looks like a directory. The path is lazily created (equivalent to mkdir -p)") 227 | markFlagRequired(pf, "out") 228 | 229 | pf.StringVar(&c.alias, "alias", "", "(Optional) The name used for the generated wrapper in the struct, constructor, and default circuit prefix. Defaults to name") 230 | pf.BoolVar(&c.debug, "debug", false, "Enable debug logging mode") 231 | pf.BoolVar(&c.goimports, "goimports", true, "Enable goimports formatting. If false, uses gofmt") 232 | pf.IntVar(&c.majorVersion, "circuit-major-version", 2, "(Optional) The version of cep21/circuit to import. Use 3 or greater for go module compatibility.") 233 | 234 | return cmd 235 | } 236 | 237 | func markFlagRequired(pf *pflag.FlagSet, name string) { 238 | err := cobra.MarkFlagRequired(pf, name) 239 | if err != nil { 240 | fmt.Fprintf(os.Stderr, "error marking %s flag as required\n", name) 241 | os.Exit(1) 242 | } 243 | } 244 | 245 | func (c *circuitCmd) Execute() error { 246 | if c.alias == "" { 247 | c.alias = c.name 248 | } 249 | 250 | if !strings.HasSuffix(c.out, ".go") { 251 | c.out = filepath.Join(c.out, strings.ToLower(c.alias)+".gen.go") 252 | } 253 | 254 | if err := c.gen(); err != nil { 255 | return fmt.Errorf("generating circuit wrapper: %v", err) 256 | } 257 | 258 | return nil 259 | } 260 | 261 | func (c *circuitCmd) gen() error { 262 | s := time.Now() 263 | pkgs, err := loadPackages(c.pkg) 264 | if err != nil { 265 | return err 266 | } 267 | c.log("loadPackages took %v", time.Since(s)) 268 | 269 | err = firstPackagesError(pkgs) 270 | if err != nil { 271 | return err 272 | } 273 | 274 | pkg := pkgs[0] 275 | 276 | obj := pkg.Types.Scope().Lookup(c.name) 277 | if obj == nil { 278 | return errors.New("could not lookup name") 279 | } 280 | 281 | typ := obj.Type() 282 | if typ == nil { 283 | return errors.New("object is not a type") 284 | } 285 | 286 | s = time.Now() 287 | outPkgPath, err := resolvePackagePath(c.out) 288 | if err != nil { 289 | return err 290 | } 291 | c.log("resolvePackagePath took %v", time.Since(s)) 292 | 293 | outPkgName := filepath.Base(outPkgPath) 294 | 295 | s = time.Now() 296 | typeMeta, err := parseType(typ, outPkgPath) 297 | if err != nil { 298 | return err 299 | } 300 | c.log("parseType took %v", time.Since(s)) 301 | 302 | templateCtx := circuitWrapperTemplateContext{ 303 | PackageName: outPkgName, 304 | VersionSuffix: circuitVersionSuffix(c.majorVersion), 305 | TypeMetadata: typeMeta, 306 | Alias: c.alias, 307 | } 308 | 309 | s = time.Now() 310 | var b bytes.Buffer 311 | err = circuitWrapperTemplate.Execute(&b, &templateCtx) 312 | if err != nil { 313 | return fmt.Errorf("rendering circuit wrapper: %v", err) 314 | } 315 | c.log("executing circuit wrapper template took %v", time.Since(s)) 316 | 317 | s = time.Now() 318 | var src []byte 319 | if c.goimports { 320 | src, err = imports.Process("", b.Bytes(), nil) 321 | } else { 322 | src, err = format.Source(b.Bytes()) 323 | } 324 | if err != nil { 325 | return fmt.Errorf("formatting rendered circuit wrapper: %v", err) 326 | } 327 | c.log("formatting code took %v", time.Since(s)) 328 | 329 | err = writeFile(c.out, src) 330 | if err != nil { 331 | return fmt.Errorf("writing circuit wrapper file: %v", err) 332 | } 333 | 334 | return nil 335 | } 336 | 337 | func (c *circuitCmd) log(msg string, args ...interface{}) { 338 | if c.debug { 339 | fmt.Printf("[debug] "+msg+"\n", args...) 340 | } 341 | } 342 | 343 | // Writes the src to the path. The directory is lazily created for the path (equivalent to `mkdir -p`) 344 | func writeFile(path string, src []byte) error { 345 | dir := filepath.Dir(path) 346 | err := os.MkdirAll(dir, 0750) 347 | if err != nil { 348 | return err 349 | } 350 | 351 | return ioutil.WriteFile(path, src, 0600) 352 | } 353 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/twitchtv/circuitgen 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/cep21/circuit v2.4.1+incompatible 7 | github.com/cep21/circuit/v3 v3.1.0 8 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 9 | github.com/kisielk/errcheck v1.2.0 10 | github.com/securego/gosec v0.0.0-20190510081509-ee80733faf72 11 | github.com/spf13/cobra v0.0.3 12 | github.com/spf13/pflag v1.0.3 13 | github.com/stretchr/objx v0.1.1 // indirect 14 | github.com/stretchr/testify v1.3.0 15 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422 16 | golang.org/x/tools v0.1.10 17 | honnef.co/go/tools v0.0.1-2019.2.3 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 4 | github.com/cactus/go-statsd-client v3.1.1+incompatible/go.mod h1:cMRcwZDklk7hXp+Law83urTHUiHMzCev/r4JMYr/zU0= 5 | github.com/cenk/backoff v2.2.1+incompatible/go.mod h1:7FtoeaSnHoZnmZzz47cM35Y9nSW7tNyaidugnHTaFDE= 6 | github.com/cep21/circuit v2.4.1+incompatible h1:oER/H6loUKgdEEqboLlwCyXftZrxfdVx+GSN9lAaf9E= 7 | github.com/cep21/circuit v2.4.1+incompatible/go.mod h1:IYFTZLwEh0jvbURztvjxE45GH5IzZb884I/8xaiwEAA= 8 | github.com/cep21/circuit/v3 v3.1.0 h1:njzXJy6cVuwrqD7OIUlSoTXThTOl2roAH1KCqMTY0J4= 9 | github.com/cep21/circuit/v3 v3.1.0/go.mod h1:BCYrZoMPDpaIZHncTqe3OyJjMCgG6ead5oaxBF1s5ac= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= 14 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 16 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 18 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 20 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 21 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 22 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 23 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 24 | github.com/iand/circuit v0.0.0-20171204111915-2e03e581ff44/go.mod h1:uYGCxUEkNx+YWAP7rl7kG3HPPzQ+U0jL5aKW9SigAas= 25 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 26 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 27 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 28 | github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E= 29 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 30 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 31 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 32 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 36 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 37 | github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= 38 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= 39 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 40 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 41 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 42 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 43 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 44 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 45 | github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea/go.mod h1:1VcHEd3ro4QMoHfiNl/j7Jkln9+KQuorp0PItHMJYNg= 46 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 49 | github.com/rubyist/circuitbreaker v2.2.1+incompatible/go.mod h1:Ycs3JgJADPuzJDwffe12k6BZT8hxVi6lFK+gWYJLN4A= 50 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 51 | github.com/securego/gosec v0.0.0-20190510081509-ee80733faf72 h1:403cuEGt3niQPAau/if+Fp34KjBzMKGejdBCu486W3s= 52 | github.com/securego/gosec v0.0.0-20190510081509-ee80733faf72/go.mod h1:shk+oGa7JTGg9taMxXk2skTwpt9KQAbryuwFIHCm/fw= 53 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 54 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 55 | github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 56 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 57 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 58 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 59 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 60 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 61 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 64 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 65 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 66 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 67 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 68 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 69 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 70 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 71 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 72 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 73 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 74 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= 75 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 76 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 77 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 78 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 79 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= 80 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 81 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 82 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 83 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 84 | golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 h1:HdqqaWmYAUI7/dmByKKEw+yxDksGSo+9GjkUc9Zp34E= 85 | golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 86 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 87 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 88 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= 89 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 90 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 91 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= 92 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 93 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 101 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 103 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= 108 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 110 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 111 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 112 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 113 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 114 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 115 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 116 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 117 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 118 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 119 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 120 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 121 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 122 | golang.org/x/tools v0.0.0-20200723165920-188b38280e32 h1:p5cVi2xPdiHRbCjqC+y3e13Q1N3R8VlJkyU8w2X7WFk= 123 | golang.org/x/tools v0.0.0-20200723165920-188b38280e32/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 124 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 125 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 126 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 128 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 130 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 132 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 135 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 137 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 138 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 139 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 140 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 141 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 143 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 144 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 145 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 146 | -------------------------------------------------------------------------------- /internal/circuitgentest/aggregator.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by circuitgen tool. DO NOT EDIT 2 | 3 | package circuitgentest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cep21/circuit" 9 | ) 10 | 11 | // CircuitWrapperAggregatorConfig contains configuration for CircuitWrapperAggregator. All fields are optional 12 | type CircuitWrapperAggregatorConfig struct { 13 | // ShouldSkipError determines whether an error should be skipped and have the circuit 14 | // track the call as successful. This takes precedence over IsBadRequest 15 | ShouldSkipError func(error) bool 16 | 17 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 18 | IsBadRequest func(error) bool 19 | 20 | // Prefix is prepended to all circuit names 21 | Prefix string 22 | 23 | // Defaults are used for all created circuits. Per-circuit configs override this 24 | Defaults circuit.Config 25 | 26 | // CircuitIncSum is the configuration used for the IncSum circuit. This overrides values set by Defaults 27 | CircuitIncSum circuit.Config 28 | } 29 | 30 | // CircuitWrapperAggregator is a circuit wrapper for *Aggregator 31 | type CircuitWrapperAggregator struct { 32 | *Aggregator 33 | 34 | // ShouldSkipError determines whether an error should be skipped and have the circuit 35 | // track the call as successful. This takes precedence over IsBadRequest 36 | ShouldSkipError func(error) bool 37 | 38 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 39 | IsBadRequest func(error) bool 40 | 41 | // CircuitIncSum is the circuit for method IncSum 42 | CircuitIncSum *circuit.Circuit 43 | } 44 | 45 | // NewCircuitWrapperAggregator creates a new circuit wrapper and initializes circuits 46 | func NewCircuitWrapperAggregator( 47 | manager *circuit.Manager, 48 | embedded *Aggregator, 49 | conf CircuitWrapperAggregatorConfig, 50 | ) (*CircuitWrapperAggregator, error) { 51 | if conf.ShouldSkipError == nil { 52 | conf.ShouldSkipError = func(err error) bool { 53 | return false 54 | } 55 | } 56 | 57 | if conf.IsBadRequest == nil { 58 | conf.IsBadRequest = func(err error) bool { 59 | return false 60 | } 61 | } 62 | 63 | w := &CircuitWrapperAggregator{ 64 | Aggregator: embedded, 65 | ShouldSkipError: conf.ShouldSkipError, 66 | IsBadRequest: conf.IsBadRequest, 67 | } 68 | 69 | var err error 70 | w.CircuitIncSum, err = manager.CreateCircuit(conf.Prefix+"Aggregator.IncSum", conf.CircuitIncSum, conf.Defaults) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return w, nil 76 | } 77 | 78 | // IncSum calls the embedded *Aggregator's method IncSum with CircuitIncSum 79 | func (w *CircuitWrapperAggregator) IncSum(ctx context.Context, p1 int) error { 80 | var skippedErr error 81 | 82 | err := w.CircuitIncSum.Run(ctx, func(ctx context.Context) error { 83 | err := w.Aggregator.IncSum(ctx, p1) 84 | 85 | if w.ShouldSkipError(err) { 86 | skippedErr = err 87 | return nil 88 | } 89 | 90 | if w.IsBadRequest(err) { 91 | return &circuit.SimpleBadRequest{Err: err} 92 | } 93 | return err 94 | }) 95 | 96 | if skippedErr != nil { 97 | err = skippedErr 98 | } 99 | 100 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 101 | err = berr.Err 102 | } 103 | 104 | return err 105 | } 106 | -------------------------------------------------------------------------------- /internal/circuitgentest/aggregator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package circuitgentest 15 | 16 | import ( 17 | "context" 18 | ) 19 | 20 | // Aggregator is a test struct for wrapper generation 21 | type Aggregator struct { 22 | IncSumError error 23 | sum int 24 | } 25 | 26 | // IncSum increments sum by v 27 | func (a *Aggregator) IncSum(ctx context.Context, v int) error { 28 | a.sum += v 29 | return a.IncSumError 30 | } 31 | 32 | // Sum returns sum 33 | func (a *Aggregator) Sum() int { 34 | return a.sum 35 | } 36 | 37 | func (a *Aggregator) privateIncSum(_ context.Context, v int) error { 38 | a.sum += v 39 | return a.IncSumError 40 | } 41 | -------------------------------------------------------------------------------- /internal/circuitgentest/aggregator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | package circuitgentest 14 | 15 | import ( 16 | "context" 17 | "testing" 18 | 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func Test_passlint(t *testing.T) { 23 | var a Aggregator 24 | err := a.privateIncSum(context.Background(), 0) 25 | require.NoError(t, err) 26 | } 27 | -------------------------------------------------------------------------------- /internal/circuitgentest/circuittest/aggregator.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by circuitgen tool. DO NOT EDIT 2 | 3 | package circuittest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cep21/circuit" 9 | "github.com/twitchtv/circuitgen/internal/circuitgentest" 10 | ) 11 | 12 | // CircuitWrapperAggregatorConfig contains configuration for CircuitWrapperAggregator. All fields are optional 13 | type CircuitWrapperAggregatorConfig struct { 14 | // ShouldSkipError determines whether an error should be skipped and have the circuit 15 | // track the call as successful. This takes precedence over IsBadRequest 16 | ShouldSkipError func(error) bool 17 | 18 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 19 | IsBadRequest func(error) bool 20 | 21 | // Prefix is prepended to all circuit names 22 | Prefix string 23 | 24 | // Defaults are used for all created circuits. Per-circuit configs override this 25 | Defaults circuit.Config 26 | 27 | // CircuitIncSum is the configuration used for the IncSum circuit. This overrides values set by Defaults 28 | CircuitIncSum circuit.Config 29 | } 30 | 31 | // CircuitWrapperAggregator is a circuit wrapper for *circuitgentest.Aggregator 32 | type CircuitWrapperAggregator struct { 33 | *circuitgentest.Aggregator 34 | 35 | // ShouldSkipError determines whether an error should be skipped and have the circuit 36 | // track the call as successful. This takes precedence over IsBadRequest 37 | ShouldSkipError func(error) bool 38 | 39 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 40 | IsBadRequest func(error) bool 41 | 42 | // CircuitIncSum is the circuit for method IncSum 43 | CircuitIncSum *circuit.Circuit 44 | } 45 | 46 | // NewCircuitWrapperAggregator creates a new circuit wrapper and initializes circuits 47 | func NewCircuitWrapperAggregator( 48 | manager *circuit.Manager, 49 | embedded *circuitgentest.Aggregator, 50 | conf CircuitWrapperAggregatorConfig, 51 | ) (*CircuitWrapperAggregator, error) { 52 | if conf.ShouldSkipError == nil { 53 | conf.ShouldSkipError = func(err error) bool { 54 | return false 55 | } 56 | } 57 | 58 | if conf.IsBadRequest == nil { 59 | conf.IsBadRequest = func(err error) bool { 60 | return false 61 | } 62 | } 63 | 64 | w := &CircuitWrapperAggregator{ 65 | Aggregator: embedded, 66 | ShouldSkipError: conf.ShouldSkipError, 67 | IsBadRequest: conf.IsBadRequest, 68 | } 69 | 70 | var err error 71 | w.CircuitIncSum, err = manager.CreateCircuit(conf.Prefix+"Aggregator.IncSum", conf.CircuitIncSum, conf.Defaults) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return w, nil 77 | } 78 | 79 | // IncSum calls the embedded *circuitgentest.Aggregator's method IncSum with CircuitIncSum 80 | func (w *CircuitWrapperAggregator) IncSum(ctx context.Context, p1 int) error { 81 | var skippedErr error 82 | 83 | err := w.CircuitIncSum.Run(ctx, func(ctx context.Context) error { 84 | err := w.Aggregator.IncSum(ctx, p1) 85 | 86 | if w.ShouldSkipError(err) { 87 | skippedErr = err 88 | return nil 89 | } 90 | 91 | if w.IsBadRequest(err) { 92 | return &circuit.SimpleBadRequest{Err: err} 93 | } 94 | return err 95 | }) 96 | 97 | if skippedErr != nil { 98 | err = skippedErr 99 | } 100 | 101 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 102 | err = berr.Err 103 | } 104 | 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /internal/circuitgentest/circuittest/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package circuittest 15 | 16 | // Test generation in a separate package. gen_test.go contains comprehensive test on generated circuit wrappers. 17 | // 18 | // Disable goimports to catch any import bugs 19 | 20 | //go:generate circuitgen circuit --goimports=true --pkg ../ --name Publisher --out ./ 21 | //go:generate circuitgen circuit --goimports=true --pkg ../ --name Publisher --alias Pubsub --out ./pubsub.gen.go 22 | //go:generate circuitgen circuit --goimports=true --pkg ../ --name Aggregator --out ./aggregator.gen.go 23 | -------------------------------------------------------------------------------- /internal/circuitgentest/circuittest/gen_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package circuittest 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "testing" 20 | "time" 21 | 22 | "github.com/cep21/circuit" 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/mock" 25 | "github.com/stretchr/testify/require" 26 | "github.com/twitchtv/circuitgen/internal/circuitgentest" 27 | "github.com/twitchtv/circuitgen/internal/circuitgentest/model" 28 | "github.com/twitchtv/circuitgen/internal/circuitgentest/rep" 29 | ) 30 | 31 | // Test generated clients 32 | 33 | func TestPublisherInterface(t *testing.T) { 34 | manager := &circuit.Manager{} 35 | 36 | ctx := context.Background() 37 | 38 | grants := map[circuitgentest.Seed][][]circuitgentest.Grant{ 39 | "seed": { 40 | { 41 | {Name: "something"}, 42 | }, 43 | }, 44 | } 45 | topics := circuitgentest.TopicsList{List: []string{"1234"}} 46 | publishInput := rep.PublishInput{UserID: "9999"} 47 | publishResult := &model.Result{Nonce: "abcdefg"} 48 | m := &circuitgentest.MockPublisher{} 49 | m.On("PublishWithResult", mock.Anything, publishInput).Return(publishResult, nil).Once() 50 | 51 | opt := rep.PublishOption{Sample: 0.1} 52 | opt2 := rep.PublishOption{Sample: 0.2} 53 | m.On("Publish", mock.Anything, grants, topics, opt, opt2).Return(nil, nil).Once() 54 | 55 | m.On("Close", mock.Anything).Return(nil).Once() 56 | 57 | publishCounter := &runMetricsCounter{} 58 | publishWithResultCounter := &runMetricsCounter{} 59 | publisher, err := NewCircuitWrapperPublisher(manager, m, CircuitWrapperPublisherConfig{ 60 | Defaults: circuit.Config{ 61 | Execution: circuit.ExecutionConfig{ 62 | Timeout: 1 * time.Second, 63 | }, 64 | }, 65 | CircuitPublish: circuit.Config{ 66 | Execution: circuit.ExecutionConfig{ 67 | Timeout: 2 * time.Second, 68 | }, 69 | Metrics: circuit.MetricsCollectors{ 70 | Run: []circuit.RunMetrics{publishCounter}, 71 | }, 72 | }, 73 | CircuitPublishWithResult: circuit.Config{ 74 | Metrics: circuit.MetricsCollectors{ 75 | Run: []circuit.RunMetrics{publishWithResultCounter}, 76 | }, 77 | }, 78 | }) 79 | require.NoError(t, err) 80 | require.NotNil(t, publisher) 81 | 82 | // Check circuit names 83 | names := circuitNames(manager) 84 | require.Contains(t, names, "Publisher.PublishWithResult") 85 | require.Contains(t, names, "Publisher.Publish") 86 | 87 | require.Equal(t, 1*time.Second, manager.GetCircuit("Publisher.PublishWithResult").Config().Execution.Timeout) 88 | require.Equal(t, 2*time.Second, manager.GetCircuit("Publisher.Publish").Config().Execution.Timeout) 89 | 90 | _, err = publisher.Publish(ctx, grants, topics, opt, opt2) 91 | require.NoError(t, err) 92 | 93 | res, err := publisher.PublishWithResult(ctx, publishInput) 94 | require.NoError(t, err) 95 | require.Equal(t, publishResult, res) 96 | 97 | require.NoError(t, publisher.Close()) 98 | 99 | // Check embedded called 100 | m.AssertExpectations(t) 101 | 102 | // Check circuits called 103 | assert.EqualValues(t, 1, publishCounter.success) 104 | assert.EqualValues(t, 1, publishWithResultCounter.success) 105 | } 106 | 107 | func TestPublisherInterfaceErrors(t *testing.T) { 108 | manager := &circuit.Manager{} 109 | 110 | testError := errors.New("test error") 111 | m := &circuitgentest.MockPublisher{} 112 | m.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil, testError).Once() 113 | 114 | publishCounter := &runMetricsCounter{} 115 | publisher, err := NewCircuitWrapperPublisher(manager, m, CircuitWrapperPublisherConfig{ 116 | CircuitPublish: circuit.Config{ 117 | Metrics: circuit.MetricsCollectors{ 118 | Run: []circuit.RunMetrics{publishCounter}, 119 | }, 120 | }, 121 | }) 122 | require.NoError(t, err) 123 | require.NotNil(t, publisher) 124 | 125 | ctx := context.Background() 126 | _, err = publisher.Publish(ctx, map[circuitgentest.Seed][][]circuitgentest.Grant{}, circuitgentest.TopicsList{}) 127 | require.Equal(t, testError, err) 128 | 129 | // Check embedded called 130 | m.AssertExpectations(t) 131 | 132 | // Check circuit called 133 | assert.EqualValues(t, 0, publishCounter.success) 134 | assert.EqualValues(t, 1, publishCounter.failure) 135 | assert.EqualValues(t, 0, publishCounter.badRequest) 136 | } 137 | 138 | func TestPublisherInterfaceBadRequest(t *testing.T) { 139 | manager := &circuit.Manager{} 140 | 141 | testError := errors.New("bad request error") 142 | m := &circuitgentest.MockPublisher{} 143 | m.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, testError).Once() 144 | 145 | publishCounter := &runMetricsCounter{} 146 | publisher, err := NewCircuitWrapperPublisher(manager, m, CircuitWrapperPublisherConfig{ 147 | IsBadRequest: func(err error) bool { 148 | return err == testError 149 | }, 150 | CircuitPublish: circuit.Config{ 151 | Metrics: circuit.MetricsCollectors{ 152 | Run: []circuit.RunMetrics{publishCounter}, 153 | }, 154 | }, 155 | }) 156 | require.NoError(t, err) 157 | require.NotNil(t, publisher) 158 | 159 | ctx := context.Background() 160 | _, err = publisher.Publish(ctx, map[circuitgentest.Seed][][]circuitgentest.Grant{}, circuitgentest.TopicsList{}) 161 | require.Equal(t, testError, err) 162 | 163 | // Check embedded called 164 | m.AssertExpectations(t) 165 | 166 | // Check circuit called 167 | assert.EqualValues(t, 0, publishCounter.success) 168 | assert.EqualValues(t, 0, publishCounter.failure) 169 | assert.EqualValues(t, 1, publishCounter.badRequest) 170 | } 171 | 172 | func TestPublisherInterfaceSkippedError(t *testing.T) { 173 | manager := &circuit.Manager{} 174 | 175 | skippedError := errors.New("skipped error") 176 | m := &circuitgentest.MockPublisher{} 177 | m.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, skippedError).Once() 178 | 179 | publishCounter := &runMetricsCounter{} 180 | publisher, err := NewCircuitWrapperPublisher(manager, m, CircuitWrapperPublisherConfig{ 181 | ShouldSkipError: func(err error) bool { 182 | return err == skippedError 183 | }, 184 | IsBadRequest: func(err error) bool { 185 | return err == skippedError 186 | }, 187 | CircuitPublish: circuit.Config{ 188 | Metrics: circuit.MetricsCollectors{ 189 | Run: []circuit.RunMetrics{publishCounter}, 190 | }, 191 | }, 192 | }) 193 | require.NoError(t, err) 194 | require.NotNil(t, publisher) 195 | 196 | ctx := context.Background() 197 | _, err = publisher.Publish(ctx, map[circuitgentest.Seed][][]circuitgentest.Grant{}, circuitgentest.TopicsList{}) 198 | require.Equal(t, skippedError, err) 199 | 200 | // Check embedded called 201 | m.AssertExpectations(t) 202 | 203 | // Check circuit called 204 | assert.EqualValues(t, 1, publishCounter.success) 205 | assert.EqualValues(t, 0, publishCounter.failure) 206 | assert.EqualValues(t, 0, publishCounter.badRequest) 207 | } 208 | 209 | // Thinner test of aliased wrapper. 210 | func TestPubsubInterface(t *testing.T) { 211 | manager := &circuit.Manager{} 212 | 213 | m := &circuitgentest.MockPublisher{} 214 | m.On("PublishWithResult", mock.Anything, mock.Anything).Return(nil, nil).Once() 215 | m.On("Publish", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() 216 | m.On("Close", mock.Anything).Return(nil).Once() 217 | 218 | pubsub, err := NewCircuitWrapperPubsub(manager, m, CircuitWrapperPubsubConfig{}) 219 | require.NoError(t, err) 220 | require.NotNil(t, pubsub) 221 | 222 | // Check circuit names 223 | names := circuitNames(manager) 224 | require.Contains(t, names, "Pubsub.PublishWithResult") 225 | require.Contains(t, names, "Pubsub.Publish") 226 | 227 | ctx := context.Background() 228 | _, err = pubsub.Publish(ctx, map[circuitgentest.Seed][][]circuitgentest.Grant{}, circuitgentest.TopicsList{}) 229 | require.NoError(t, err) 230 | 231 | _, err = pubsub.PublishWithResult(ctx, rep.PublishInput{}) 232 | require.NoError(t, err) 233 | 234 | require.NoError(t, pubsub.Close()) 235 | 236 | // Check embedded called 237 | m.AssertExpectations(t) 238 | } 239 | 240 | func TestAggregatorStruct(t *testing.T) { 241 | manager := &circuit.Manager{} 242 | agg := &circuitgentest.Aggregator{} 243 | 244 | incSumCounter := &runMetricsCounter{} 245 | wrapperAgg, err := NewCircuitWrapperAggregator(manager, agg, CircuitWrapperAggregatorConfig{ 246 | CircuitIncSum: circuit.Config{ 247 | Metrics: circuit.MetricsCollectors{ 248 | Run: []circuit.RunMetrics{incSumCounter}, 249 | }, 250 | }, 251 | }) 252 | require.NoError(t, err) 253 | 254 | err = wrapperAgg.IncSum(context.Background(), 10) 255 | require.NoError(t, err) 256 | require.Equal(t, 10, agg.Sum()) 257 | require.Equal(t, 10, wrapperAgg.Sum()) 258 | require.Equal(t, 1, incSumCounter.success) 259 | 260 | sumErr := errors.New("sum error") 261 | agg.IncSumError = sumErr 262 | err = wrapperAgg.IncSum(context.Background(), 10) 263 | require.Equal(t, sumErr, err) 264 | 265 | } 266 | 267 | func circuitNames(m *circuit.Manager) []string { 268 | names := make([]string, 0, len(m.AllCircuits())) 269 | for _, circ := range m.AllCircuits() { 270 | names = append(names, circ.Name()) 271 | } 272 | return names 273 | } 274 | 275 | type runMetricsCounter struct { 276 | success int 277 | failure int 278 | timeout int 279 | badRequest int 280 | interrupt int 281 | concurrencyLimitReject int 282 | shortCircuit int 283 | } 284 | 285 | func (r *runMetricsCounter) Success(now time.Time, duration time.Duration) { r.success++ } 286 | func (r *runMetricsCounter) ErrFailure(now time.Time, duration time.Duration) { r.failure++ } 287 | func (r *runMetricsCounter) ErrTimeout(now time.Time, duration time.Duration) { r.timeout++ } 288 | func (r *runMetricsCounter) ErrBadRequest(now time.Time, duration time.Duration) { r.badRequest++ } 289 | func (r *runMetricsCounter) ErrInterrupt(now time.Time, duration time.Duration) { r.interrupt++ } 290 | func (r *runMetricsCounter) ErrConcurrencyLimitReject(now time.Time) { r.concurrencyLimitReject++ } 291 | func (r *runMetricsCounter) ErrShortCircuit(now time.Time) { r.shortCircuit++ } 292 | 293 | var _ circuit.RunMetrics = (*runMetricsCounter)(nil) 294 | -------------------------------------------------------------------------------- /internal/circuitgentest/circuittest/publisher.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by circuitgen tool. DO NOT EDIT 2 | 3 | package circuittest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cep21/circuit" 9 | "github.com/twitchtv/circuitgen/internal/circuitgentest" 10 | "github.com/twitchtv/circuitgen/internal/circuitgentest/model" 11 | "github.com/twitchtv/circuitgen/internal/circuitgentest/rep" 12 | ) 13 | 14 | // CircuitWrapperPublisherConfig contains configuration for CircuitWrapperPublisher. All fields are optional 15 | type CircuitWrapperPublisherConfig struct { 16 | // ShouldSkipError determines whether an error should be skipped and have the circuit 17 | // track the call as successful. This takes precedence over IsBadRequest 18 | ShouldSkipError func(error) bool 19 | 20 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 21 | IsBadRequest func(error) bool 22 | 23 | // Prefix is prepended to all circuit names 24 | Prefix string 25 | 26 | // Defaults are used for all created circuits. Per-circuit configs override this 27 | Defaults circuit.Config 28 | 29 | // CircuitPublish is the configuration used for the Publish circuit. This overrides values set by Defaults 30 | CircuitPublish circuit.Config 31 | // CircuitPublishWithResult is the configuration used for the PublishWithResult circuit. This overrides values set by Defaults 32 | CircuitPublishWithResult circuit.Config 33 | } 34 | 35 | // CircuitWrapperPublisher is a circuit wrapper for circuitgentest.Publisher 36 | type CircuitWrapperPublisher struct { 37 | circuitgentest.Publisher 38 | 39 | // ShouldSkipError determines whether an error should be skipped and have the circuit 40 | // track the call as successful. This takes precedence over IsBadRequest 41 | ShouldSkipError func(error) bool 42 | 43 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 44 | IsBadRequest func(error) bool 45 | 46 | // CircuitPublish is the circuit for method Publish 47 | CircuitPublish *circuit.Circuit 48 | // CircuitPublishWithResult is the circuit for method PublishWithResult 49 | CircuitPublishWithResult *circuit.Circuit 50 | } 51 | 52 | // NewCircuitWrapperPublisher creates a new circuit wrapper and initializes circuits 53 | func NewCircuitWrapperPublisher( 54 | manager *circuit.Manager, 55 | embedded circuitgentest.Publisher, 56 | conf CircuitWrapperPublisherConfig, 57 | ) (*CircuitWrapperPublisher, error) { 58 | if conf.ShouldSkipError == nil { 59 | conf.ShouldSkipError = func(err error) bool { 60 | return false 61 | } 62 | } 63 | 64 | if conf.IsBadRequest == nil { 65 | conf.IsBadRequest = func(err error) bool { 66 | return false 67 | } 68 | } 69 | 70 | w := &CircuitWrapperPublisher{ 71 | Publisher: embedded, 72 | ShouldSkipError: conf.ShouldSkipError, 73 | IsBadRequest: conf.IsBadRequest, 74 | } 75 | 76 | var err error 77 | 78 | w.CircuitPublish, err = manager.CreateCircuit(conf.Prefix+"Publisher.Publish", conf.CircuitPublish, conf.Defaults) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | w.CircuitPublishWithResult, err = manager.CreateCircuit(conf.Prefix+"Publisher.PublishWithResult", conf.CircuitPublishWithResult, conf.Defaults) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return w, nil 89 | } 90 | 91 | // Publish calls the embedded circuitgentest.Publisher's method Publish with CircuitPublish 92 | func (w *CircuitWrapperPublisher) Publish(ctx context.Context, p1 map[circuitgentest.Seed][][]circuitgentest.Grant, p2 circuitgentest.TopicsList, p3 ...rep.PublishOption) (map[string]struct{}, error) { 93 | var r0 map[string]struct{} 94 | var skippedErr error 95 | 96 | err := w.CircuitPublish.Run(ctx, func(ctx context.Context) error { 97 | var err error 98 | r0, err = w.Publisher.Publish(ctx, p1, p2, p3...) 99 | 100 | if w.ShouldSkipError(err) { 101 | skippedErr = err 102 | return nil 103 | } 104 | 105 | if w.IsBadRequest(err) { 106 | return &circuit.SimpleBadRequest{Err: err} 107 | } 108 | return err 109 | }) 110 | 111 | if skippedErr != nil { 112 | err = skippedErr 113 | } 114 | 115 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 116 | err = berr.Err 117 | } 118 | 119 | return r0, err 120 | } 121 | 122 | // PublishWithResult calls the embedded circuitgentest.Publisher's method PublishWithResult with CircuitPublishWithResult 123 | func (w *CircuitWrapperPublisher) PublishWithResult(ctx context.Context, p1 rep.PublishInput) (*model.Result, error) { 124 | var r0 *model.Result 125 | var skippedErr error 126 | 127 | err := w.CircuitPublishWithResult.Run(ctx, func(ctx context.Context) error { 128 | var err error 129 | r0, err = w.Publisher.PublishWithResult(ctx, p1) 130 | 131 | if w.ShouldSkipError(err) { 132 | skippedErr = err 133 | return nil 134 | } 135 | 136 | if w.IsBadRequest(err) { 137 | return &circuit.SimpleBadRequest{Err: err} 138 | } 139 | return err 140 | }) 141 | 142 | if skippedErr != nil { 143 | err = skippedErr 144 | } 145 | 146 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 147 | err = berr.Err 148 | } 149 | 150 | return r0, err 151 | } 152 | 153 | var _ circuitgentest.Publisher = (*CircuitWrapperPublisher)(nil) 154 | -------------------------------------------------------------------------------- /internal/circuitgentest/circuittest/pubsub.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by circuitgen tool. DO NOT EDIT 2 | 3 | package circuittest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cep21/circuit" 9 | "github.com/twitchtv/circuitgen/internal/circuitgentest" 10 | "github.com/twitchtv/circuitgen/internal/circuitgentest/model" 11 | "github.com/twitchtv/circuitgen/internal/circuitgentest/rep" 12 | ) 13 | 14 | // CircuitWrapperPubsubConfig contains configuration for CircuitWrapperPubsub. All fields are optional 15 | type CircuitWrapperPubsubConfig struct { 16 | // ShouldSkipError determines whether an error should be skipped and have the circuit 17 | // track the call as successful. This takes precedence over IsBadRequest 18 | ShouldSkipError func(error) bool 19 | 20 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 21 | IsBadRequest func(error) bool 22 | 23 | // Prefix is prepended to all circuit names 24 | Prefix string 25 | 26 | // Defaults are used for all created circuits. Per-circuit configs override this 27 | Defaults circuit.Config 28 | 29 | // CircuitPublish is the configuration used for the Publish circuit. This overrides values set by Defaults 30 | CircuitPublish circuit.Config 31 | // CircuitPublishWithResult is the configuration used for the PublishWithResult circuit. This overrides values set by Defaults 32 | CircuitPublishWithResult circuit.Config 33 | } 34 | 35 | // CircuitWrapperPubsub is a circuit wrapper for circuitgentest.Publisher 36 | type CircuitWrapperPubsub struct { 37 | circuitgentest.Publisher 38 | 39 | // ShouldSkipError determines whether an error should be skipped and have the circuit 40 | // track the call as successful. This takes precedence over IsBadRequest 41 | ShouldSkipError func(error) bool 42 | 43 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 44 | IsBadRequest func(error) bool 45 | 46 | // CircuitPublish is the circuit for method Publish 47 | CircuitPublish *circuit.Circuit 48 | // CircuitPublishWithResult is the circuit for method PublishWithResult 49 | CircuitPublishWithResult *circuit.Circuit 50 | } 51 | 52 | // NewCircuitWrapperPubsub creates a new circuit wrapper and initializes circuits 53 | func NewCircuitWrapperPubsub( 54 | manager *circuit.Manager, 55 | embedded circuitgentest.Publisher, 56 | conf CircuitWrapperPubsubConfig, 57 | ) (*CircuitWrapperPubsub, error) { 58 | if conf.ShouldSkipError == nil { 59 | conf.ShouldSkipError = func(err error) bool { 60 | return false 61 | } 62 | } 63 | 64 | if conf.IsBadRequest == nil { 65 | conf.IsBadRequest = func(err error) bool { 66 | return false 67 | } 68 | } 69 | 70 | w := &CircuitWrapperPubsub{ 71 | Publisher: embedded, 72 | ShouldSkipError: conf.ShouldSkipError, 73 | IsBadRequest: conf.IsBadRequest, 74 | } 75 | 76 | var err error 77 | 78 | w.CircuitPublish, err = manager.CreateCircuit(conf.Prefix+"Pubsub.Publish", conf.CircuitPublish, conf.Defaults) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | w.CircuitPublishWithResult, err = manager.CreateCircuit(conf.Prefix+"Pubsub.PublishWithResult", conf.CircuitPublishWithResult, conf.Defaults) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return w, nil 89 | } 90 | 91 | // Publish calls the embedded circuitgentest.Publisher's method Publish with CircuitPublish 92 | func (w *CircuitWrapperPubsub) Publish(ctx context.Context, p1 map[circuitgentest.Seed][][]circuitgentest.Grant, p2 circuitgentest.TopicsList, p3 ...rep.PublishOption) (map[string]struct{}, error) { 93 | var r0 map[string]struct{} 94 | var skippedErr error 95 | 96 | err := w.CircuitPublish.Run(ctx, func(ctx context.Context) error { 97 | var err error 98 | r0, err = w.Publisher.Publish(ctx, p1, p2, p3...) 99 | 100 | if w.ShouldSkipError(err) { 101 | skippedErr = err 102 | return nil 103 | } 104 | 105 | if w.IsBadRequest(err) { 106 | return &circuit.SimpleBadRequest{Err: err} 107 | } 108 | return err 109 | }) 110 | 111 | if skippedErr != nil { 112 | err = skippedErr 113 | } 114 | 115 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 116 | err = berr.Err 117 | } 118 | 119 | return r0, err 120 | } 121 | 122 | // PublishWithResult calls the embedded circuitgentest.Publisher's method PublishWithResult with CircuitPublishWithResult 123 | func (w *CircuitWrapperPubsub) PublishWithResult(ctx context.Context, p1 rep.PublishInput) (*model.Result, error) { 124 | var r0 *model.Result 125 | var skippedErr error 126 | 127 | err := w.CircuitPublishWithResult.Run(ctx, func(ctx context.Context) error { 128 | var err error 129 | r0, err = w.Publisher.PublishWithResult(ctx, p1) 130 | 131 | if w.ShouldSkipError(err) { 132 | skippedErr = err 133 | return nil 134 | } 135 | 136 | if w.IsBadRequest(err) { 137 | return &circuit.SimpleBadRequest{Err: err} 138 | } 139 | return err 140 | }) 141 | 142 | if skippedErr != nil { 143 | err = skippedErr 144 | } 145 | 146 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 147 | err = berr.Err 148 | } 149 | 150 | return r0, err 151 | } 152 | 153 | var _ circuitgentest.Publisher = (*CircuitWrapperPubsub)(nil) 154 | -------------------------------------------------------------------------------- /internal/circuitgentest/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package circuitgentest 15 | 16 | // Test generation in the same package. There are only compile-time tests. Comprehensive tests for 17 | // circuit wrappers are in circuitest/gen_test.go 18 | // 19 | // Disable goimports to catch any import bugs 20 | 21 | //go:generate circuitgen circuit --goimports=true --pkg . --name Publisher --out ./publisher.gen.go 22 | //go:generate circuitgen circuit --goimports=false --pkg . --name Publisher --alias PublisherCircuitV3 --out ./publishercircuitv3.gen.go --circuit-major-version 3 23 | //go:generate circuitgen circuit --goimports=true --pkg . --name Aggregator --out ./ 24 | -------------------------------------------------------------------------------- /internal/circuitgentest/model/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package model 15 | 16 | // Result is a test struct model 17 | type Result struct { 18 | // Nonce contains a one time value 19 | Nonce string 20 | } 21 | -------------------------------------------------------------------------------- /internal/circuitgentest/publisher.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by circuitgen tool. DO NOT EDIT 2 | 3 | package circuitgentest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cep21/circuit" 9 | "github.com/twitchtv/circuitgen/internal/circuitgentest/model" 10 | "github.com/twitchtv/circuitgen/internal/circuitgentest/rep" 11 | ) 12 | 13 | // CircuitWrapperPublisherConfig contains configuration for CircuitWrapperPublisher. All fields are optional 14 | type CircuitWrapperPublisherConfig struct { 15 | // ShouldSkipError determines whether an error should be skipped and have the circuit 16 | // track the call as successful. This takes precedence over IsBadRequest 17 | ShouldSkipError func(error) bool 18 | 19 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 20 | IsBadRequest func(error) bool 21 | 22 | // Prefix is prepended to all circuit names 23 | Prefix string 24 | 25 | // Defaults are used for all created circuits. Per-circuit configs override this 26 | Defaults circuit.Config 27 | 28 | // CircuitPublish is the configuration used for the Publish circuit. This overrides values set by Defaults 29 | CircuitPublish circuit.Config 30 | // CircuitPublishWithResult is the configuration used for the PublishWithResult circuit. This overrides values set by Defaults 31 | CircuitPublishWithResult circuit.Config 32 | } 33 | 34 | // CircuitWrapperPublisher is a circuit wrapper for Publisher 35 | type CircuitWrapperPublisher struct { 36 | Publisher 37 | 38 | // ShouldSkipError determines whether an error should be skipped and have the circuit 39 | // track the call as successful. This takes precedence over IsBadRequest 40 | ShouldSkipError func(error) bool 41 | 42 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 43 | IsBadRequest func(error) bool 44 | 45 | // CircuitPublish is the circuit for method Publish 46 | CircuitPublish *circuit.Circuit 47 | // CircuitPublishWithResult is the circuit for method PublishWithResult 48 | CircuitPublishWithResult *circuit.Circuit 49 | } 50 | 51 | // NewCircuitWrapperPublisher creates a new circuit wrapper and initializes circuits 52 | func NewCircuitWrapperPublisher( 53 | manager *circuit.Manager, 54 | embedded Publisher, 55 | conf CircuitWrapperPublisherConfig, 56 | ) (*CircuitWrapperPublisher, error) { 57 | if conf.ShouldSkipError == nil { 58 | conf.ShouldSkipError = func(err error) bool { 59 | return false 60 | } 61 | } 62 | 63 | if conf.IsBadRequest == nil { 64 | conf.IsBadRequest = func(err error) bool { 65 | return false 66 | } 67 | } 68 | 69 | w := &CircuitWrapperPublisher{ 70 | Publisher: embedded, 71 | ShouldSkipError: conf.ShouldSkipError, 72 | IsBadRequest: conf.IsBadRequest, 73 | } 74 | 75 | var err error 76 | 77 | w.CircuitPublish, err = manager.CreateCircuit(conf.Prefix+"Publisher.Publish", conf.CircuitPublish, conf.Defaults) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | w.CircuitPublishWithResult, err = manager.CreateCircuit(conf.Prefix+"Publisher.PublishWithResult", conf.CircuitPublishWithResult, conf.Defaults) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return w, nil 88 | } 89 | 90 | // Publish calls the embedded Publisher's method Publish with CircuitPublish 91 | func (w *CircuitWrapperPublisher) Publish(ctx context.Context, p1 map[Seed][][]Grant, p2 TopicsList, p3 ...rep.PublishOption) (map[string]struct{}, error) { 92 | var r0 map[string]struct{} 93 | var skippedErr error 94 | 95 | err := w.CircuitPublish.Run(ctx, func(ctx context.Context) error { 96 | var err error 97 | r0, err = w.Publisher.Publish(ctx, p1, p2, p3...) 98 | 99 | if w.ShouldSkipError(err) { 100 | skippedErr = err 101 | return nil 102 | } 103 | 104 | if w.IsBadRequest(err) { 105 | return &circuit.SimpleBadRequest{Err: err} 106 | } 107 | return err 108 | }) 109 | 110 | if skippedErr != nil { 111 | err = skippedErr 112 | } 113 | 114 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 115 | err = berr.Err 116 | } 117 | 118 | return r0, err 119 | } 120 | 121 | // PublishWithResult calls the embedded Publisher's method PublishWithResult with CircuitPublishWithResult 122 | func (w *CircuitWrapperPublisher) PublishWithResult(ctx context.Context, p1 rep.PublishInput) (*model.Result, error) { 123 | var r0 *model.Result 124 | var skippedErr error 125 | 126 | err := w.CircuitPublishWithResult.Run(ctx, func(ctx context.Context) error { 127 | var err error 128 | r0, err = w.Publisher.PublishWithResult(ctx, p1) 129 | 130 | if w.ShouldSkipError(err) { 131 | skippedErr = err 132 | return nil 133 | } 134 | 135 | if w.IsBadRequest(err) { 136 | return &circuit.SimpleBadRequest{Err: err} 137 | } 138 | return err 139 | }) 140 | 141 | if skippedErr != nil { 142 | err = skippedErr 143 | } 144 | 145 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 146 | err = berr.Err 147 | } 148 | 149 | return r0, err 150 | } 151 | 152 | var _ Publisher = (*CircuitWrapperPublisher)(nil) 153 | -------------------------------------------------------------------------------- /internal/circuitgentest/publisher.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package circuitgentest 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/stretchr/testify/mock" 20 | "github.com/twitchtv/circuitgen/internal/circuitgentest/model" 21 | "github.com/twitchtv/circuitgen/internal/circuitgentest/rep" 22 | ) 23 | 24 | // TopicsList is a test struct 25 | type TopicsList struct { 26 | List []string 27 | } 28 | 29 | // Seed is a test type 30 | type Seed string 31 | 32 | // Grant is a test struct 33 | type Grant struct { 34 | // Name is a name 35 | Name string 36 | } 37 | 38 | // Publisher is an interface for testing. This interface has many different types to test generation. 39 | type Publisher interface { 40 | // PublishWithResult is a test method and should be wrapped 41 | PublishWithResult(context.Context, rep.PublishInput) (*model.Result, error) 42 | // PublishWithResult is a test method and should be wrapped 43 | Publish(context.Context, map[Seed][][]Grant, TopicsList, ...rep.PublishOption) (map[string]struct{}, error) 44 | // PublishWithResult is a test method and should not be wrapped 45 | Close() error 46 | } 47 | 48 | // MockPublisher is a test mock for the Publisher interface 49 | type MockPublisher struct { 50 | mock.Mock 51 | } 52 | 53 | // PublishWithResult mocks the method 54 | func (m *MockPublisher) PublishWithResult(ctx context.Context, input rep.PublishInput) (*model.Result, error) { 55 | args := m.Called(ctx, input) 56 | var r0 *model.Result 57 | if args.Get(0) != nil { 58 | var ok bool 59 | r0, ok = args.Get(0).(*model.Result) 60 | if !ok { 61 | panic("args.Get(0) is not a *model.Result") 62 | } 63 | } 64 | return r0, args.Error(1) 65 | } 66 | 67 | // Publish mocks the method 68 | func (m *MockPublisher) Publish(ctx context.Context, g map[Seed][][]Grant, s TopicsList, opts ...rep.PublishOption) (map[string]struct{}, error) { 69 | var ca []interface{} 70 | ca = append(ca, ctx, g, s) 71 | va := make([]interface{}, len(opts)) 72 | for i := range opts { 73 | va[i] = opts[i] 74 | } 75 | ca = append(ca, va...) 76 | args := m.Called(ca...) 77 | 78 | var r0 map[string]struct{} 79 | if args.Get(0) != nil { 80 | var ok bool 81 | r0, ok = args.Get(0).(map[string]struct{}) 82 | if !ok { 83 | panic("args.Get(0) is not a map[string]struct{}") 84 | } 85 | } 86 | 87 | return r0, args.Error(1) 88 | } 89 | 90 | // Close mocks the method 91 | func (m *MockPublisher) Close() error { 92 | args := m.Called() 93 | return args.Error(0) 94 | } 95 | 96 | var _ Publisher = (*MockPublisher)(nil) 97 | -------------------------------------------------------------------------------- /internal/circuitgentest/publishercircuitv3.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by circuitgen tool. DO NOT EDIT 2 | 3 | package circuitgentest 4 | 5 | import ( 6 | "context" 7 | "github.com/cep21/circuit/v3" 8 | "github.com/twitchtv/circuitgen/internal/circuitgentest/model" 9 | "github.com/twitchtv/circuitgen/internal/circuitgentest/rep" 10 | ) 11 | 12 | // CircuitWrapperPublisherCircuitV3Config contains configuration for CircuitWrapperPublisherCircuitV3. All fields are optional 13 | type CircuitWrapperPublisherCircuitV3Config struct { 14 | // ShouldSkipError determines whether an error should be skipped and have the circuit 15 | // track the call as successful. This takes precedence over IsBadRequest 16 | ShouldSkipError func(error) bool 17 | 18 | // IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults 19 | IsBadRequest func(error) bool 20 | 21 | // Prefix is prepended to all circuit names 22 | Prefix string 23 | 24 | // Defaults are used for all created circuits. Per-circuit configs override this 25 | Defaults circuit.Config 26 | 27 | // CircuitPublish is the configuration used for the Publish circuit. This overrides values set by Defaults 28 | CircuitPublish circuit.Config 29 | // CircuitPublishWithResult is the configuration used for the PublishWithResult circuit. This overrides values set by Defaults 30 | CircuitPublishWithResult circuit.Config 31 | } 32 | 33 | // CircuitWrapperPublisherCircuitV3 is a circuit wrapper for Publisher 34 | type CircuitWrapperPublisherCircuitV3 struct { 35 | Publisher 36 | 37 | // ShouldSkipError determines whether an error should be skipped and have the circuit 38 | // track the call as successful. This takes precedence over IsBadRequest 39 | ShouldSkipError func(error) bool 40 | 41 | // IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this 42 | IsBadRequest func(error) bool 43 | 44 | // CircuitPublish is the circuit for method Publish 45 | CircuitPublish *circuit.Circuit 46 | // CircuitPublishWithResult is the circuit for method PublishWithResult 47 | CircuitPublishWithResult *circuit.Circuit 48 | } 49 | 50 | // NewCircuitWrapperPublisherCircuitV3 creates a new circuit wrapper and initializes circuits 51 | func NewCircuitWrapperPublisherCircuitV3( 52 | manager *circuit.Manager, 53 | embedded Publisher, 54 | conf CircuitWrapperPublisherCircuitV3Config, 55 | ) (*CircuitWrapperPublisherCircuitV3, error) { 56 | if conf.ShouldSkipError == nil { 57 | conf.ShouldSkipError = func(err error) bool { 58 | return false 59 | } 60 | } 61 | 62 | if conf.IsBadRequest == nil { 63 | conf.IsBadRequest = func(err error) bool { 64 | return false 65 | } 66 | } 67 | 68 | w := &CircuitWrapperPublisherCircuitV3{ 69 | Publisher: embedded, 70 | ShouldSkipError: conf.ShouldSkipError, 71 | IsBadRequest: conf.IsBadRequest, 72 | } 73 | 74 | var err error 75 | 76 | w.CircuitPublish, err = manager.CreateCircuit(conf.Prefix+"PublisherCircuitV3.Publish", conf.CircuitPublish, conf.Defaults) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | w.CircuitPublishWithResult, err = manager.CreateCircuit(conf.Prefix+"PublisherCircuitV3.PublishWithResult", conf.CircuitPublishWithResult, conf.Defaults) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return w, nil 87 | } 88 | 89 | // Publish calls the embedded Publisher's method Publish with CircuitPublish 90 | func (w *CircuitWrapperPublisherCircuitV3) Publish(ctx context.Context, p1 map[Seed][][]Grant, p2 TopicsList, p3 ...rep.PublishOption) (map[string]struct{}, error) { 91 | var r0 map[string]struct{} 92 | var skippedErr error 93 | 94 | err := w.CircuitPublish.Run(ctx, func(ctx context.Context) error { 95 | var err error 96 | r0, err = w.Publisher.Publish(ctx, p1, p2, p3...) 97 | 98 | if w.ShouldSkipError(err) { 99 | skippedErr = err 100 | return nil 101 | } 102 | 103 | if w.IsBadRequest(err) { 104 | return &circuit.SimpleBadRequest{Err: err} 105 | } 106 | return err 107 | }) 108 | 109 | if skippedErr != nil { 110 | err = skippedErr 111 | } 112 | 113 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 114 | err = berr.Err 115 | } 116 | 117 | return r0, err 118 | } 119 | 120 | // PublishWithResult calls the embedded Publisher's method PublishWithResult with CircuitPublishWithResult 121 | func (w *CircuitWrapperPublisherCircuitV3) PublishWithResult(ctx context.Context, p1 rep.PublishInput) (*model.Result, error) { 122 | var r0 *model.Result 123 | var skippedErr error 124 | 125 | err := w.CircuitPublishWithResult.Run(ctx, func(ctx context.Context) error { 126 | var err error 127 | r0, err = w.Publisher.PublishWithResult(ctx, p1) 128 | 129 | if w.ShouldSkipError(err) { 130 | skippedErr = err 131 | return nil 132 | } 133 | 134 | if w.IsBadRequest(err) { 135 | return &circuit.SimpleBadRequest{Err: err} 136 | } 137 | return err 138 | }) 139 | 140 | if skippedErr != nil { 141 | err = skippedErr 142 | } 143 | 144 | if berr, ok := err.(*circuit.SimpleBadRequest); ok { 145 | err = berr.Err 146 | } 147 | 148 | return r0, err 149 | } 150 | 151 | var _ Publisher = (*CircuitWrapperPublisherCircuitV3)(nil) 152 | -------------------------------------------------------------------------------- /internal/circuitgentest/rep/rep.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package rep 15 | 16 | // PublishOption is a test struct 17 | type PublishOption struct { 18 | // Sample is a ratio 19 | Sample float64 20 | } 21 | 22 | // PublishInput is a test struct 23 | type PublishInput struct { 24 | // UserID is a user identifier 25 | UserID string 26 | // Topics contains topics 27 | Topics []string 28 | } 29 | -------------------------------------------------------------------------------- /license_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "bytes" 18 | "io" 19 | "io/ioutil" 20 | "os" 21 | "path/filepath" 22 | "regexp" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | var headerCopyright = regexp.MustCompile(`// Copyright \d{4} Twitch Interactive, Inc. All Rights Reserved.`) 28 | 29 | const headerLicense = ` 30 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 31 | // use this file except in compliance with the License. A copy of the License is 32 | // located at 33 | // 34 | // http://www.apache.org/licenses/LICENSE-2.0 35 | // 36 | // or in the "license" file accompanying this file. This file is distributed on 37 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 38 | // express or implied. See the License for the specific language governing 39 | // permissions and limitations under the License. 40 | ` 41 | 42 | var generatedCodeMatcher = regexp.MustCompile("// Code generated .* DO NOT EDIT") 43 | 44 | func TestSourceCodeLicenseHeaders(t *testing.T) { 45 | err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if !strings.HasSuffix(path, ".go") { 51 | // Skip non-go files. 52 | return nil 53 | } 54 | 55 | file, err := os.Open(path) 56 | if err != nil { 57 | return err 58 | } 59 | fileBytes, err := ioutil.ReadAll(file) 60 | if err != nil { 61 | return err 62 | } 63 | fileBuf := bytes.NewReader(fileBytes) 64 | 65 | if generatedCodeMatcher.MatchReader(fileBuf) { 66 | // Skip generated files. 67 | return nil 68 | } 69 | 70 | _, err = fileBuf.Seek(0, io.SeekStart) 71 | if err != nil { 72 | return err 73 | } 74 | if !headerCopyright.MatchReader(fileBuf) { 75 | t.Errorf("%v is missing licensing header", path) 76 | return nil 77 | } 78 | 79 | if !bytes.Contains(fileBytes, []byte(headerLicense)) { 80 | t.Errorf("%v is missing licensing header", path) 81 | } 82 | 83 | return nil 84 | }) 85 | if err != nil { 86 | t.Fatalf("error scanning directory for source code files: %v", err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | ) 20 | 21 | func main() { 22 | cmd := &circuitCmd{} 23 | 24 | if err := cmd.Cobra().Execute(); err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /parsing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "go/types" 20 | "path/filepath" 21 | "strconv" 22 | "strings" 23 | 24 | "golang.org/x/tools/go/packages" // latest loader that supports modules 25 | "golang.org/x/tools/go/types/typeutil" 26 | ) 27 | 28 | func resolvePackagePath(path string) (string, error) { 29 | path, err := filepath.Abs(path) 30 | if err != nil { 31 | return "", err 32 | } 33 | conf := &packages.Config{ 34 | Mode: packages.NeedName, 35 | } 36 | 37 | if strings.HasSuffix(path, ".go") { 38 | path = filepath.Dir(path) 39 | } 40 | 41 | pkgs, err := packages.Load(conf, path) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | // We don't check pkgs[0].Errors because the package may not exist 47 | return pkgs[0].PkgPath, nil 48 | } 49 | 50 | func loadPackages(pkgPaths ...string) ([]*packages.Package, error) { 51 | conf := &packages.Config{ 52 | Mode: packages.NeedTypes | packages.NeedTypesSizes | packages.NeedImports | 53 | packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles, 54 | } 55 | 56 | pkgs, err := packages.Load(conf, pkgPaths...) 57 | if err != nil { 58 | return nil, fmt.Errorf("loading packages: %v", err) 59 | } 60 | 61 | return pkgs, nil 62 | } 63 | 64 | func firstPackagesError(pkgs []*packages.Package) error { 65 | var err error 66 | 67 | packages.Visit(pkgs, nil, func(pkg *packages.Package) { 68 | for _, pkgErr := range pkg.Errors { 69 | if err == nil { 70 | err = fmt.Errorf("loading package %s: %v", pkg.PkgPath, pkgErr) 71 | } 72 | } 73 | }) 74 | 75 | return err 76 | } 77 | 78 | // Parse the type for its type info, imports, and methods. 79 | func parseType(t types.Type, outPkgPath string) (TypeMetadata, error) { 80 | mset := methodSet(t) 81 | if len(mset) == 0 { 82 | return TypeMetadata{}, fmt.Errorf("empty methodset. %v has no exported methods", t) 83 | } 84 | 85 | // Get all the import paths 86 | imports, err := parseImports(t, mset, outPkgPath) 87 | if err != nil { 88 | return TypeMetadata{}, err 89 | } 90 | 91 | methods := make([]Method, 0, len(mset)) 92 | 93 | // For each method of the interface, get the name, params, results, and 94 | // other info needed to generate a wrapper. 95 | for _, m := range mset { 96 | sig, ok := m.Type().(*types.Signature) 97 | if !ok { 98 | return TypeMetadata{}, fmt.Errorf("method %s is not a signature", m.String()) 99 | } 100 | 101 | methods = append(methods, Method{ 102 | Name: m.Obj().Name(), 103 | Params: parseTuple(sig.Params(), outPkgPath), 104 | Results: parseTuple(sig.Results(), outPkgPath), 105 | Variadic: sig.Variadic(), 106 | }) 107 | } 108 | 109 | tn, ok := t.(*types.Named) 110 | if !ok { 111 | return TypeMetadata{}, errors.New("not a named type") 112 | } 113 | 114 | tm := TypeMetadata{ 115 | PackageName: tn.Obj().Pkg().Name(), 116 | PackagePath: stripVendor(tn.Obj().Pkg().Path()), 117 | TypeInfo: typeInfo(t, outPkgPath), 118 | Imports: imports, 119 | Methods: methods, 120 | } 121 | 122 | return tm, nil 123 | } 124 | 125 | func methodSet(t types.Type) []*types.Selection { 126 | var mset []*types.Selection 127 | if types.IsInterface(t) { 128 | mset = typeutil.IntuitiveMethodSet(t.Underlying(), nil) 129 | } else { 130 | mset = typeutil.IntuitiveMethodSet(t, nil) // supports structs 131 | } 132 | 133 | var exported []*types.Selection 134 | for _, m := range mset { 135 | if m.Obj().Exported() { 136 | exported = append(exported, m) 137 | } 138 | } 139 | 140 | return exported 141 | } 142 | 143 | // get all the package import paths given the type. 144 | func resolvePkgPaths(p types.Type) ([]string, error) { 145 | switch t := p.(type) { 146 | case *types.Signature: 147 | var r []string 148 | for i := 0; i < t.Params().Len(); i++ { 149 | paths, err := resolvePkgPaths(t.Params().At(i).Type()) 150 | if err != nil { 151 | return nil, err 152 | } 153 | r = append(r, paths...) 154 | } 155 | for i := 0; i < t.Results().Len(); i++ { 156 | paths, err := resolvePkgPaths(t.Results().At(i).Type()) 157 | if err != nil { 158 | return nil, err 159 | } 160 | r = append(r, paths...) 161 | } 162 | return r, nil 163 | case *types.Pointer: 164 | return resolvePkgPaths(t.Elem()) 165 | case *types.Map: 166 | keyPaths, err := resolvePkgPaths(t.Key()) 167 | if err != nil { 168 | return nil, err 169 | } 170 | elemPaths, err := resolvePkgPaths(t.Elem()) 171 | if err != nil { 172 | return nil, err 173 | } 174 | return append(keyPaths, elemPaths...), nil 175 | case *types.Slice: 176 | return resolvePkgPaths(t.Elem()) 177 | case *types.Named: 178 | // builtins (e.g. error) have a nil package 179 | if pkg := t.Obj().Pkg(); pkg != nil { 180 | return []string{stripVendor(pkg.Path())}, nil 181 | } 182 | case *types.Basic: 183 | case *types.Interface: 184 | case *types.Struct: // struct{} 185 | // Break out of the switch and return below 186 | default: 187 | return nil, fmt.Errorf("resolvePkgPaths: invalid type: %v", t) 188 | } 189 | 190 | return []string{}, nil 191 | } 192 | 193 | // returns unique import package paths for the given type 194 | func parseImports(t types.Type, mset []*types.Selection, outPkgPath string) ([]Import, error) { 195 | pkgPaths := make([]string, 0, len(mset)) 196 | for _, m := range mset { 197 | paths, err := resolvePkgPaths(m.Type()) 198 | if err != nil { 199 | return nil, err 200 | } 201 | pkgPaths = append(pkgPaths, paths...) 202 | } 203 | 204 | var imports []Import 205 | inPkg := typePackagePath(t) == outPkgPath 206 | if !inPkg { 207 | // add import for the type 208 | imports = append(imports, Import{ 209 | Path: typePackagePath(t), 210 | }) 211 | } 212 | 213 | for _, path := range uniqueStringSlice(pkgPaths) { 214 | // Don't add import if in the same package to prevent a circular dependency 215 | if !inPkg || (inPkg && path != outPkgPath) { 216 | imports = append(imports, Import{Path: path}) 217 | } 218 | } 219 | 220 | return imports, nil 221 | } 222 | 223 | // parseTuple parses a list of variables, like a method's params and results. 224 | func parseTuple(tuple *types.Tuple, outPkgPath string) []TypeInfo { 225 | vars := []TypeInfo{} 226 | 227 | for i := 0; i < tuple.Len(); i++ { 228 | v := tuple.At(i) 229 | 230 | vars = append(vars, typeInfo(v.Type(), outPkgPath)) 231 | } 232 | 233 | return vars 234 | } 235 | 236 | func typeInfo(t types.Type, outPkgPath string) TypeInfo { 237 | return TypeInfo{ 238 | // Check if the pkg qualifier should be added depending on 239 | // if the type is in outPkgPath 240 | Name: types.TypeString(t, func(pkg *types.Package) string { 241 | if pkg.Path() == outPkgPath { 242 | return "" 243 | } 244 | return pkg.Name() 245 | }), 246 | NameWithoutQualifier: types.TypeString(t, func(pkg *types.Package) string { 247 | return "" 248 | }), 249 | IsInterface: types.IsInterface(t), 250 | } 251 | } 252 | 253 | func uniqueStringSlice(s []string) []string { 254 | m := map[string]struct{}{} 255 | paths := []string{} 256 | for _, p := range s { 257 | if _, ok := m[p]; !ok { 258 | paths = append(paths, p) 259 | m[p] = struct{}{} 260 | } 261 | } 262 | 263 | return paths 264 | } 265 | 266 | // Returns the package path of the given type. The type must be named 267 | func typePackagePath(t types.Type) string { 268 | tn, ok := t.(*types.Named) 269 | if !ok { 270 | return "" 271 | } 272 | if tn.Obj() == nil { 273 | return "" 274 | } 275 | if tn.Obj().Pkg() == nil { 276 | return "" 277 | } 278 | return stripVendor(tn.Obj().Pkg().Path()) 279 | } 280 | 281 | // stripVendor resolves the given package path by stripping vendor prefixes if needed. 282 | func stripVendor(path string) string { 283 | const vendor = "/vendor/" 284 | i := strings.Index(path, vendor) 285 | if i > -1 { 286 | return path[i+len(vendor):] 287 | } 288 | 289 | return path 290 | } 291 | 292 | func circuitVersionSuffix(majorVersion int) string { 293 | if majorVersion < 3 { 294 | return "" 295 | } 296 | return "/v" + strconv.Itoa(majorVersion) 297 | } 298 | -------------------------------------------------------------------------------- /template_models.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "strings" 19 | ) 20 | 21 | // This file contains structs used in the wrapper generating templates. 22 | 23 | // TypeMetadata stores metadata about a Go type. 24 | type TypeMetadata struct { 25 | // Name of the package this type is defined in. 26 | PackageName string 27 | 28 | // Path to the package this type is defined in. 29 | PackagePath string 30 | 31 | // Holds type information about this type 32 | TypeInfo TypeInfo 33 | 34 | // Imports of this type from all the methods 35 | Imports []Import 36 | 37 | // Methods of this type 38 | Methods []Method 39 | } 40 | 41 | // Method represents a method function on a type 42 | type Method struct { 43 | // The name of the method on the type 44 | Name string 45 | 46 | // Input params 47 | Params []TypeInfo 48 | 49 | // Return results 50 | Results []TypeInfo 51 | 52 | // Whether this method is variadic 53 | Variadic bool 54 | } 55 | 56 | // TypeInfo stores the name and whether it is an interface 57 | type TypeInfo struct { 58 | // The name of the type with or without the package qualifier. The qualifier is set appropriately 59 | // Examples: 60 | // 61 | // "aws.Context" or "Context" 62 | // "error would be "error" 63 | Name string 64 | 65 | // The name of the type without the package qualifier. Ex. "aws.Context" would be "Context" 66 | NameWithoutQualifier string 67 | 68 | // Whether this type is an interface 69 | IsInterface bool 70 | } 71 | 72 | // Import represents a package import 73 | type Import struct { 74 | // ex. "github.com/aws/aws-sdk-go/service/dynamodb" 75 | Path string 76 | } 77 | 78 | // ParamsSignature generates the signature for the methods params 79 | // ex. "r0 aws.Context, r1 *dynamodb.BatchGetItemInput" 80 | func (m Method) ParamsSignature(overrides ...string) string { 81 | s := "" 82 | mt := m.Params 83 | l := len(mt) 84 | 85 | for i := 0; i < l; i++ { 86 | varName := fmt.Sprintf("p%d", i) 87 | 88 | if i < len(overrides) { 89 | varName = overrides[i] 90 | } 91 | 92 | if i == l-1 && m.Variadic { 93 | const sliceChars = "[]" 94 | // the qualfied name contains the "[]" at the beginning, so chop it off 95 | s += fmt.Sprintf("%s ...%s", varName, mt[i].Name[len(sliceChars):]) 96 | } else { 97 | s += fmt.Sprintf("%s %s", varName, mt[i].Name) 98 | if i < l-1 { 99 | s += ", " 100 | } 101 | } 102 | } 103 | 104 | return s 105 | } 106 | 107 | // CallSignatureWithClosure generates the signature for calling the embedded interface with a closure 108 | func (m Method) CallSignatureWithClosure() string { 109 | s := "" 110 | mt := m.Params 111 | l := len(mt) 112 | 113 | for i := 0; i < l; i++ { 114 | if i == l-1 && m.Variadic { 115 | s += fmt.Sprintf("p%d...", i) 116 | } else { 117 | if i == 0 { 118 | s += "ctx" 119 | } else { 120 | s += fmt.Sprintf("p%d", i) 121 | } 122 | 123 | if i < l-1 { 124 | s += ", " 125 | } 126 | } 127 | } 128 | 129 | return s 130 | } 131 | 132 | // ResultsSignature generates the signature for the methods results 133 | // ex. "(*dynamodb.BatchGetItemOutput, error)" 134 | func (m Method) ResultsSignature() string { 135 | mt := m.Results 136 | if len(mt) == 1 { 137 | return mt[0].Name 138 | } 139 | 140 | s := "(" 141 | l := len(mt) 142 | for i := 0; i < l; i++ { 143 | if i == l-1 { 144 | s += mt[i].Name 145 | } else { 146 | s += mt[i].Name + ", " 147 | } 148 | } 149 | 150 | return s + ")" 151 | } 152 | 153 | // ResultsClosureVariableDeclarations generates the variable declarations needed for making a call with a closure. These variables are needed 154 | // outside the function scope (ex. (*circuit.Circuit).Run()). 155 | // ex. "var r0 *dynamodb.UpdateItemInput 156 | func (m Method) ResultsClosureVariableDeclarations() string { 157 | s := "" 158 | for i, t := range m.Results[:len(m.Results)-1] { 159 | s += fmt.Sprintf("var r%d %s\n", i, t.Name) 160 | } 161 | 162 | return s 163 | } 164 | 165 | // HasOneMethodResultVariable returns whether there is exactly one return value 166 | func (m Method) HasOneMethodResultVariable() bool { 167 | return len(m.Results) == 1 168 | } 169 | 170 | // ResultsCircuitVariableAssignments generates the variable names needed when assigning the embedded interface method call. 171 | // ex. "r0, err" 172 | func (m Method) ResultsCircuitVariableAssignments() string { 173 | s := "" 174 | 175 | for i := range m.Results[:len(m.Results)-1] { 176 | s += fmt.Sprintf("r%d, ", i) 177 | } 178 | 179 | s += "err" 180 | 181 | return s 182 | } 183 | 184 | // ResultsClosureVariableReturns generates the variable names needed when returning the results of a closure wrapped method call. 185 | // ex. "r0, err" 186 | func (m Method) ResultsClosureVariableReturns() string { 187 | s := "" 188 | for i := range m.Results[:len(m.Results)-1] { 189 | s += fmt.Sprintf("r%d, ", i) 190 | } 191 | 192 | return s 193 | } 194 | 195 | // IsWrappingSupported returns true only if the method supports context and returns an error. 196 | func (m Method) IsWrappingSupported() bool { 197 | if len(m.Params) == 0 || len(m.Results) == 0 { 198 | return false 199 | } 200 | 201 | supportsContext := strings.HasSuffix(m.Params[0].Name, "Context") 202 | returnsAnError := m.Results[len(m.Results)-1].Name == "error" 203 | 204 | return supportsContext && returnsAnError 205 | } 206 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Twitch Interactive, Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not 4 | // use this file except in compliance with the License. A copy of the License is 5 | // located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed on 10 | // an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // +build tools 15 | 16 | package main 17 | 18 | import ( 19 | _ "github.com/kisielk/errcheck" 20 | _ "github.com/securego/gosec" 21 | _ "golang.org/x/lint/golint" 22 | _ "golang.org/x/tools/cmd/goimports" 23 | _ "honnef.co/go/tools/cmd/staticcheck" 24 | ) 25 | --------------------------------------------------------------------------------