├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── lint.yml │ └── vulns.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── Taskfile.yml ├── errlist.go ├── errs.go ├── errs_test.go ├── example_test.go ├── go.mod ├── go.work ├── sample ├── sample1.go ├── sample2.go ├── sample3.go ├── sample4.go ├── sample5.go └── sample6.go └── zapobject ├── example_test.go ├── go.mod ├── go.sum └── zapobject.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = tab 7 | indent_size = 4 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | 11 | [*.go] 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | indent_style = space 16 | indent_size = 4 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | trim_trailing_whitespace = true 22 | 23 | [*.toml] 24 | indent_style = space 25 | indent_size = 2 26 | trim_trailing_whitespace = true 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go text eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 20 * * 0' 16 | 17 | jobs: 18 | CodeQL-Build: 19 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 20 | runs-on: ubuntu-latest 21 | 22 | permissions: 23 | # required for all workflows 24 | security-events: write 25 | 26 | # only required for workflows in private repositories 27 | actions: read 28 | contents: read 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v3 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v2 37 | # Override language selection by uncommenting this and choosing your languages 38 | with: 39 | languages: go 40 | 41 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 42 | # If this step fails, then you should remove it and run the build manually (see below). 43 | - name: Autobuild 44 | uses: github/codeql-action/autobuild@v2 45 | 46 | # ℹ️ Command-line programs to run using the OS shell. 47 | # 📚 https://git.io/JvXDl 48 | 49 | # ✏️ If the Autobuild fails above, remove it and uncomment the following 50 | # three lines and modify them (or add more) to build your code if your 51 | # project uses a compiled language 52 | 53 | #- run: | 54 | # make bootstrap 55 | # make release 56 | 57 | - name: Perform CodeQL Analysis 58 | uses: github/codeql-action/analyze@v2 59 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 11 | # pull-requests: read 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-go@v3 19 | with: 20 | go-version-file: 'go.mod' 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 25 | version: latest 26 | 27 | # Optional: working directory, useful for monorepos 28 | # working-directory: somedir 29 | 30 | # Optional: golangci-lint command line arguments. 31 | args: --enable gosec 32 | 33 | # Optional: show only new issues if it's a pull request. The default value is `false`. 34 | # only-new-issues: true 35 | 36 | # Optional: if set to true then the all caching functionality will be complete disabled, 37 | # takes precedence over all other caching options. 38 | # skip-cache: true 39 | 40 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 41 | # skip-pkg-cache: true 42 | 43 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 44 | # skip-build-cache: true 45 | - name: testing 46 | run: go test -shuffle on ./... 47 | - name: testing pgdrv 48 | run: go test -shuffle on ./zapobject/... 49 | - name: install govulncheck 50 | run: go install golang.org/x/vuln/cmd/govulncheck@latest 51 | - name: running govulncheck in errs 52 | run: govulncheck ./... 53 | - name: running govulncheck in zapobject 54 | run: govulncheck ./zapobject/... 55 | -------------------------------------------------------------------------------- /.github/workflows/vulns.yml: -------------------------------------------------------------------------------- 1 | name: vulns 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | vulns: 9 | name: Vulnerability scanner 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version-file: 'go.mod' 16 | - name: WriteGoList 17 | run: go list -json -m all > go.list 18 | - name: Nancy 19 | uses: sonatype-nexus-community/nancy-github-action@main 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Other files and folders 15 | .task/ 16 | # go.work 17 | go.work.sum 18 | *.bak 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.15.x" 5 | 6 | env: 7 | global: 8 | - GO111MODULE=on 9 | 10 | install: 11 | - go mod download 12 | 13 | script: 14 | - go test ./... 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [errs] -- Error handling for Golang 2 | 3 | [![check vulns](https://github.com/goark/errs/workflows/vulns/badge.svg)](https://github.com/goark/errs/actions) 4 | [![lint status](https://github.com/goark/errs/workflows/lint/badge.svg)](https://github.com/goark/errs/actions) 5 | [![GitHub license](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/goark/errs/master/LICENSE) 6 | [![GitHub release](http://img.shields.io/github/release/goark/errs.svg)](https://github.com/goark/errs/releases/latest) 7 | 8 | Package [errs] implements functions to manipulate error instances. 9 | This package is required Go 1.20 or later. 10 | 11 | **Migrated repository to [github.com/goark/errs][errs]** 12 | 13 | ## Usage 14 | 15 | ### Create new error instance with cause 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | 24 | "github.com/goark/errs" 25 | ) 26 | 27 | func checkFileOpen(path string) error { 28 | file, err := os.Open(path) 29 | if err != nil { 30 | return errs.New( 31 | "file open error", 32 | errs.WithCause(err), 33 | errs.WithContext("path", path), 34 | ) 35 | } 36 | defer file.Close() 37 | 38 | return nil 39 | } 40 | 41 | func main() { 42 | if err := checkFileOpen("not-exist.txt"); err != nil { 43 | fmt.Printf("%v\n", err) // file open error: open not-exist.txt: no such file or directory 44 | fmt.Printf("%#v\n", err) // *errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}} 45 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"file open error"},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"},"Cause":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}}} 46 | } 47 | } 48 | ``` 49 | 50 | ### Wrapping error instance 51 | 52 | ```go 53 | package main 54 | 55 | import ( 56 | "fmt" 57 | "os" 58 | 59 | "github.com/goark/errs" 60 | ) 61 | 62 | func checkFileOpen(path string) error { 63 | file, err := os.Open(path) 64 | if err != nil { 65 | return errs.Wrap( 66 | err, 67 | errs.WithContext("path", path), 68 | ) 69 | } 70 | defer file.Close() 71 | 72 | return nil 73 | } 74 | 75 | func main() { 76 | if err := checkFileOpen("not-exist.txt"); err != nil { 77 | fmt.Printf("%v\n", err) // open not-exist.txt: no such file or directory 78 | fmt.Printf("%#v\n", err) // *errs.Error{Err:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Cause:, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}} 79 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"}} 80 | } 81 | } 82 | ``` 83 | 84 | ### Wrapping error instance with cause 85 | 86 | ```go 87 | package main 88 | 89 | import ( 90 | "errors" 91 | "fmt" 92 | "os" 93 | 94 | "github.com/goark/errs" 95 | ) 96 | 97 | func checkFileOpen(path string) error { 98 | file, err := os.Open(path) 99 | if err != nil { 100 | return errs.Wrap( 101 | errors.New("file open error"), 102 | errs.WithCause(err), 103 | errs.WithContext("path", path), 104 | ) 105 | } 106 | defer file.Close() 107 | 108 | return nil 109 | } 110 | 111 | func main() { 112 | if err := checkFileOpen("not-exist.txt"); err != nil { 113 | fmt.Printf("%v\n", err) // file open error: open not-exist.txt: no such file or directory 114 | fmt.Printf("%#v\n", err) // *errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}} 115 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"file open error"},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"},"Cause":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}}} 116 | } 117 | } 118 | ``` 119 | 120 | ### Create new error instance with multiple causes 121 | 122 | ```go 123 | package main 124 | 125 | import ( 126 | "errors" 127 | "fmt" 128 | "io" 129 | "os" 130 | 131 | "github.com/goark/errs" 132 | ) 133 | 134 | func generateMultiError() error { 135 | return errs.New("error with multiple causes", errs.WithCause(errors.Join(os.ErrInvalid, io.EOF))) 136 | } 137 | 138 | func main() { 139 | err := generateMultiError() 140 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"error with multiple causes"},"Context":{"function":"main.generateMultiError"},"Cause":{"Type":"*errors.joinError","Msg":"invalid argument\nEOF","Cause":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]}} 141 | fmt.Println(errors.Is(err, io.EOF)) // true 142 | } 143 | ``` 144 | 145 | ### Handling multiple errors 146 | 147 | ```go 148 | package main 149 | 150 | import ( 151 | "errors" 152 | "fmt" 153 | "io" 154 | "os" 155 | 156 | "github.com/goark/errs" 157 | ) 158 | 159 | func generateMultiError() error { 160 | return errs.Join(os.ErrInvalid, io.EOF) 161 | } 162 | 163 | func main() { 164 | err := generateMultiError() 165 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]} 166 | fmt.Println(errors.Is(err, io.EOF)) // true 167 | } 168 | ``` 169 | 170 | ```go 171 | package main 172 | 173 | import ( 174 | "fmt" 175 | "sync" 176 | 177 | "github.com/goark/errs" 178 | ) 179 | 180 | func generateMultiError() error { 181 | errlist := &errs.Errors{} 182 | var wg sync.WaitGroup 183 | for i := 1; i <= 2; i++ { 184 | i := i 185 | wg.Add(1) 186 | go func() { 187 | defer wg.Done() 188 | errlist.Add(fmt.Errorf("error %d", i)) 189 | }() 190 | } 191 | wg.Wait() 192 | return errlist.ErrorOrNil() 193 | } 194 | 195 | func main() { 196 | err := generateMultiError() 197 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 1"}]} 198 | } 199 | ``` 200 | 201 | [errs]: https://github.com/goark/errs "goark/errs: Error handling for Golang" 202 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | default: 5 | cmds: 6 | - task: errs 7 | - task: zapobject 8 | - task: test 9 | - task: nancy 10 | 11 | test: 12 | desc: Test and lint. 13 | cmds: 14 | - go test -shuffle on ./... 15 | - go test -shuffle on ./zapobject/... 16 | - govulncheck ./... 17 | - govulncheck ./zapobject/... 18 | - docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.51.1 golangci-lint run --enable gosec --timeout 3m0s ./... 19 | sources: 20 | - ./go.mod 21 | - '**/*.go' 22 | 23 | nancy: 24 | desc: Check vulnerability of external packages with Nancy. 25 | cmds: 26 | - depm list -j | nancy sleuth -n 27 | sources: 28 | - ./go.mod 29 | - '**/*.go' 30 | 31 | clean: 32 | desc: Initialize module and build cache. 33 | cmds: 34 | - go clean -cache 35 | - go clean -modcache 36 | 37 | errs: 38 | cmds: 39 | - rm -f ./go.sum 40 | - go mod tidy -v -go=1.20 41 | 42 | zapobject: 43 | dir: zapobject 44 | cmds: 45 | - rm -f ./go.sum 46 | - go mod tidy -v -go=1.20 47 | -------------------------------------------------------------------------------- /errlist.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | // Errors is multiple error instance. 14 | type Errors struct { 15 | mu sync.RWMutex 16 | errs []error 17 | } 18 | 19 | // Join function returns Errors instance. 20 | func Join(errlist ...error) error { 21 | if len(errlist) == 0 { 22 | return nil 23 | } 24 | ct := 0 25 | for _, err := range errlist { 26 | if err != nil { 27 | ct++ 28 | } 29 | } 30 | if ct == 0 { 31 | return nil 32 | } 33 | es := &Errors{errs: make([]error, 0, ct)} 34 | for _, err := range errlist { 35 | if err != nil { 36 | es.errs = append(es.errs, err) 37 | } 38 | } 39 | return es 40 | } 41 | 42 | // Add method adds errors to Errors. 43 | func (es *Errors) Add(errlist ...error) { 44 | if es == nil { 45 | return 46 | } 47 | es.mu.Lock() 48 | defer es.mu.Unlock() 49 | for _, err := range errlist { 50 | if err != nil { 51 | es.errs = append(es.errs, err) 52 | } 53 | } 54 | } 55 | 56 | // ErrorOrNil method returns this as a error type. 57 | func (es *Errors) ErrorOrNil() error { 58 | if es == nil { 59 | return nil 60 | } 61 | es.mu.RLock() 62 | defer es.mu.RUnlock() 63 | if len(es.errs) == 0 { 64 | return nil 65 | } 66 | return es 67 | } 68 | 69 | // Error method returns error message. 70 | // This method is a implementation of error interface. 71 | func (es *Errors) Error() string { 72 | if es == nil { 73 | return nilAngleString 74 | } 75 | es.mu.RLock() 76 | defer es.mu.RUnlock() 77 | if len(es.errs) == 0 { 78 | return nilAngleString 79 | } 80 | var b []byte 81 | for i, err := range es.errs { 82 | if i > 0 { 83 | b = append(b, '\n') 84 | } 85 | b = append(b, err.Error()...) 86 | } 87 | return string(b) 88 | } 89 | 90 | // String method returns error message. 91 | // This method is a implementation of fmt.Stringer interface. 92 | func (es *Errors) String() string { 93 | return es.Error() 94 | } 95 | 96 | // GoString method returns serialize string of Errors. 97 | // This method is a implementation of fmt.GoStringer interface. 98 | func (es *Errors) GoString() string { 99 | if es == nil { 100 | return nilAngleString 101 | } 102 | es.mu.RLock() 103 | defer es.mu.RUnlock() 104 | if len(es.errs) == 0 { 105 | return nilAngleString 106 | } 107 | return fmt.Sprintf("%T{Errs:%#v}", es, es.errs) 108 | } 109 | 110 | // MarshalJSON method returns serialize string of Errors with JSON format. 111 | // This method is implementation of json.Marshaler interface. 112 | func (es *Errors) MarshalJSON() ([]byte, error) { 113 | return []byte(es.EncodeJSON()), nil 114 | } 115 | 116 | // Format method returns formatted string of Errors instance. 117 | // This method is a implementation of fmt.Formatter interface. 118 | func (es *Errors) Format(s fmt.State, verb rune) { 119 | switch verb { 120 | case 'v': 121 | switch { 122 | case s.Flag('#'): 123 | _, _ = strings.NewReader(es.GoString()).WriteTo(s) 124 | case s.Flag('+'): 125 | _, _ = strings.NewReader(es.EncodeJSON()).WriteTo(s) 126 | default: 127 | _, _ = strings.NewReader(es.Error()).WriteTo(s) 128 | } 129 | case 's': 130 | _, _ = strings.NewReader(es.String()).WriteTo(s) 131 | default: 132 | fmt.Fprintf(s, `%%!%c(%s)`, verb, es.GoString()) 133 | } 134 | } 135 | 136 | // EncodeJSON method returns serialize string of Errors with JSON format. 137 | func (es *Errors) EncodeJSON() string { 138 | if es == nil { 139 | return "null" 140 | } 141 | es.mu.RLock() 142 | defer es.mu.RUnlock() 143 | elms := []string{} 144 | elms = append(elms, strings.Join([]string{`"Type":`, strconv.Quote(reflect.TypeOf(es).String())}, "")) 145 | if len(es.errs) > 0 { 146 | elms2 := []string{} 147 | for _, err := range es.errs { 148 | msgBuf := &bytes.Buffer{} 149 | json.HTMLEscape(msgBuf, []byte(EncodeJSON(err))) 150 | elms2 = append(elms2, msgBuf.String()) 151 | } 152 | elms = append(elms, strings.Join([]string{`"Errs":[`, strings.Join(elms2, ","), "]"}, "")) 153 | } 154 | return strings.Join([]string{"{", strings.Join(elms, ","), "}"}, "") 155 | } 156 | 157 | // Unwrap method returns error list in Errors instance. 158 | // This method is used in errors.Unwrap function. 159 | func (es *Errors) Unwrap() []error { 160 | if es == nil { 161 | return nil 162 | } 163 | es.mu.RLock() 164 | defer es.mu.RUnlock() 165 | if len(es.errs) == 0 { 166 | return nil 167 | } 168 | cpy := make([]error, len(es.errs), cap(es.errs)) 169 | copy(cpy, es.errs) 170 | return cpy 171 | } 172 | 173 | /* Copyright 2023 Spiegel 174 | * 175 | * Licensed under the Apache License, Version 2.0 (the "License"); 176 | * you may not use this file except in compliance with the License. 177 | * You may obtain a copy of the License at 178 | * 179 | * http://www.apache.org/licenses/LICENSE-2.0 180 | * 181 | * Unless required by applicable law or agreed to in writing, software 182 | * distributed under the License is distributed on an "AS IS" BASIS, 183 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 184 | * See the License for the specific language governing permissions and 185 | * limitations under the License. 186 | */ 187 | -------------------------------------------------------------------------------- /errs.go: -------------------------------------------------------------------------------- 1 | // Package errs implements functions to manipulate error instances. 2 | package errs 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "runtime" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | nilAngleString = "" 17 | ) 18 | 19 | // Error type is a implementation of error interface. 20 | // This type is for wrapping cause error instance. 21 | type Error struct { 22 | wrapFlag bool 23 | Err error 24 | Cause error 25 | Context map[string]interface{} 26 | } 27 | 28 | var _ error = (*Error)(nil) //Error type is compatible with error interface 29 | var _ fmt.Stringer = (*Error)(nil) //Error type is compatible with fmt.Stringer interface 30 | var _ fmt.GoStringer = (*Error)(nil) //Error type is compatible with fmt.GoStringer interface 31 | var _ fmt.Formatter = (*Error)(nil) //Error type is compatible with fmt.Formatter interface 32 | var _ json.Marshaler = (*Error)(nil) //Error type is compatible with json.Marshaler interface 33 | 34 | // ErrorContextFunc type is self-referential function type for New and Wrap functions. (functional options pattern) 35 | type ErrorContextFunc func(*Error) 36 | 37 | // New function returns an error instance with message and context informations. 38 | func New(msg string, opts ...ErrorContextFunc) error { 39 | if len(msg) == 0 { 40 | return nil 41 | } 42 | return newError(errors.New(msg), false, 2, opts...) 43 | } 44 | 45 | // Wrap function returns a wrapping error instance with context informations. 46 | func Wrap(err error, opts ...ErrorContextFunc) error { 47 | if err == nil { 48 | return nil 49 | } 50 | return newError(err, true, 2, opts...) 51 | } 52 | 53 | // newError returns error instance. (internal) 54 | func newError(err error, wrapFlag bool, depth int, opts ...ErrorContextFunc) error { 55 | we := &Error{Err: err, wrapFlag: wrapFlag} 56 | //caller function name 57 | if fname, _, _ := caller(depth); len(fname) > 0 { 58 | we = we.SetContext("function", fname) 59 | } 60 | //other params 61 | for _, opt := range opts { 62 | opt(we) 63 | } 64 | return we 65 | } 66 | 67 | // WithContext function returns ErrorContextFunc function value. 68 | // This function is used in New and Wrap functions that represents context (key/value) data. 69 | func WithContext(name string, value interface{}) ErrorContextFunc { 70 | return func(e *Error) { 71 | _ = e.SetContext(name, value) 72 | } 73 | } 74 | 75 | // WithCause function returns ErrorContextFunc function value. 76 | // This function is used in New and Wrap functions that represents context (key/value) data. 77 | func WithCause(err error) ErrorContextFunc { 78 | return func(e *Error) { 79 | _ = e.SetCause(err) 80 | } 81 | } 82 | 83 | // SetContext method sets context information 84 | func (e *Error) SetContext(name string, value interface{}) *Error { 85 | if e == nil { 86 | return e 87 | } 88 | if e.Context == nil { 89 | e.Context = map[string]interface{}{} 90 | } 91 | if len(name) > 0 { 92 | e.Context[name] = value 93 | } 94 | return e 95 | } 96 | 97 | // SetCause method sets cause error instance 98 | func (e *Error) SetCause(err error) *Error { 99 | if e == nil { 100 | return e 101 | } 102 | e.Cause = err 103 | return e 104 | } 105 | 106 | // Unwrap method returns cause error in Error instance. 107 | // This method is used in errors.Unwrap function. 108 | func (e *Error) Unwrap() error { 109 | if e == nil { 110 | return nil 111 | } 112 | if e.Cause == nil { 113 | if e.wrapFlag { 114 | return e.Err 115 | } 116 | return errors.Unwrap(e.Err) 117 | } 118 | return e.Cause 119 | } 120 | 121 | // Is method reports whether any error in error's chain matches cause of target error. 122 | // This method is used in errors.Is function. 123 | func (e *Error) Is(target error) bool { 124 | if e == target { 125 | return true 126 | } 127 | if e != nil { 128 | if errors.Is(e.Err, target) { 129 | return true 130 | } 131 | if errors.Is(e.Cause, target) { 132 | return true 133 | } 134 | } 135 | return false 136 | } 137 | 138 | // Error method returns error message. 139 | // This method is a implementation of error interface. 140 | func (e *Error) Error() string { 141 | if e == nil { 142 | return nilAngleString 143 | } 144 | errMsg := e.Err.Error() 145 | var causeMsg string 146 | if e.Cause != nil { 147 | causeMsg = e.Cause.Error() 148 | } 149 | if len(causeMsg) == 0 { 150 | return errMsg 151 | } 152 | if len(errMsg) == 0 { 153 | return causeMsg 154 | } 155 | return strings.Join([]string{errMsg, causeMsg}, ": ") 156 | } 157 | 158 | // String method returns error message. 159 | // This method is a implementation of fmt.Stringer interface. 160 | func (e *Error) String() string { 161 | return e.Error() 162 | } 163 | 164 | // GoString method returns serialize string of Error. 165 | // This method is a implementation of fmt.GoStringer interface. 166 | func (e *Error) GoString() string { 167 | if e == nil { 168 | return nilAngleString 169 | } 170 | return fmt.Sprintf("%T{Err:%#v, Cause:%#v, Context:%#v}", e, e.Err, e.Cause, e.Context) 171 | } 172 | 173 | // MarshalJSON method returns serialize string of Error with JSON format. 174 | // This method is implementation of json.Marshaler interface. 175 | func (e *Error) MarshalJSON() ([]byte, error) { 176 | return []byte(e.EncodeJSON()), nil 177 | } 178 | 179 | // EncodeJSON method returns serialize string of Error with JSON format. 180 | func (e *Error) EncodeJSON() string { 181 | if e == nil { 182 | return "null" 183 | } 184 | elms := []string{} 185 | elms = append(elms, strings.Join([]string{`"Type":`, strconv.Quote(reflect.TypeOf(e).String())}, "")) 186 | msgBuf := &bytes.Buffer{} 187 | json.HTMLEscape(msgBuf, bytes.Join([][]byte{[]byte(`"Err":`), []byte(EncodeJSON(e.Err))}, []byte{})) 188 | elms = append(elms, msgBuf.String()) 189 | if len(e.Context) > 0 { 190 | if b, err := json.Marshal(e.Context); err == nil { 191 | elms = append(elms, string(bytes.Join([][]byte{[]byte(`"Context":`), b}, []byte{}))) 192 | } 193 | } 194 | if e.Cause != nil && !reflect.ValueOf(e.Cause).IsZero() { 195 | elms = append(elms, strings.Join([]string{`"Cause":`, EncodeJSON(e.Cause)}, "")) 196 | } 197 | return strings.Join([]string{"{", strings.Join(elms, ","), "}"}, "") 198 | } 199 | 200 | // Format method returns formatted string of Error instance. 201 | // This method is a implementation of fmt.Formatter interface. 202 | func (e *Error) Format(s fmt.State, verb rune) { 203 | switch verb { 204 | case 'v': 205 | switch { 206 | case s.Flag('#'): 207 | _, _ = strings.NewReader(e.GoString()).WriteTo(s) 208 | case s.Flag('+'): 209 | _, _ = strings.NewReader(e.EncodeJSON()).WriteTo(s) 210 | default: 211 | _, _ = strings.NewReader(e.Error()).WriteTo(s) 212 | } 213 | case 's': 214 | _, _ = strings.NewReader(e.String()).WriteTo(s) 215 | default: 216 | fmt.Fprintf(s, `%%!%c(%s)`, verb, e.GoString()) 217 | } 218 | } 219 | 220 | // Cause function finds cause error in target error instance. 221 | // 222 | // Deprecated: should not be used 223 | func Cause(err error) error { 224 | for err != nil { 225 | unwraped := errors.Unwrap(err) 226 | if unwraped == nil { 227 | return err 228 | } 229 | err = unwraped 230 | } 231 | return err 232 | } 233 | 234 | // Unwraps function finds cause errors ([]error slice) in target error instance. 235 | func Unwraps(err error) []error { 236 | if err == nil { 237 | return nil 238 | } 239 | if es, ok := err.(interface { 240 | Unwrap() []error 241 | }); ok { 242 | return es.Unwrap() 243 | } 244 | if e := errors.Unwrap(err); e != nil { 245 | return []error{e} 246 | } 247 | return nil 248 | } 249 | 250 | // caller returns caller info. 251 | func caller(depth int) (string, string, int) { 252 | pc, src, line, ok := runtime.Caller(depth + 1) 253 | if !ok { 254 | return "", "", 0 255 | } 256 | return runtime.FuncForPC(pc).Name(), src, line 257 | } 258 | 259 | // EncodeJSON function dumps out error instance with JSON format. 260 | func EncodeJSON(err error) string { 261 | if e, ok := err.(*Error); ok { 262 | return e.EncodeJSON() 263 | } 264 | if e, ok := err.(json.Marshaler); ok { 265 | b, ee := json.Marshal(e) 266 | if ee != nil { 267 | return encodeJSON(err) 268 | } 269 | return strings.TrimSpace(string(b)) 270 | } 271 | return encodeJSON(err) 272 | } 273 | 274 | func encodeJSON(err error) string { 275 | if err == nil { 276 | return "null" 277 | } 278 | elms := []string{} 279 | elms = append(elms, strings.Join([]string{`"Type":`, strconv.Quote(reflect.TypeOf(err).String())}, "")) 280 | msgBuf := &bytes.Buffer{} 281 | json.HTMLEscape(msgBuf, bytes.Join([][]byte{[]byte(`"Msg":`), []byte(strconv.Quote(err.Error()))}, []byte{})) 282 | elms = append(elms, msgBuf.String()) 283 | switch x := err.(type) { 284 | case interface{ Unwrap() error }: 285 | unwraped := x.Unwrap() 286 | if err != nil { 287 | elms = append(elms, strings.Join([]string{`"Cause":`, EncodeJSON(unwraped)}, "")) 288 | } 289 | case interface{ Unwrap() []error }: 290 | unwraped := x.Unwrap() 291 | if len(unwraped) > 0 { 292 | causes := []string{} 293 | for _, c := range unwraped { 294 | causes = append(causes, EncodeJSON(c)) 295 | } 296 | elms = append(elms, strings.Join([]string{`"Cause":[`, strings.Join(causes, ","), "]"}, "")) 297 | } 298 | } 299 | return strings.Join([]string{"{", strings.Join(elms, ","), "}"}, "") 300 | } 301 | 302 | // Is is conpatible with errors.Is. 303 | func Is(err, target error) bool { return errors.Is(err, target) } 304 | 305 | // As is conpatible with errors.As. 306 | func As(err error, target interface{}) bool { return errors.As(err, target) } 307 | 308 | // Unwrap is conpatible with errors.Unwrap. 309 | func Unwrap(err error) error { return errors.Unwrap(err) } 310 | 311 | /* Copyright 2019-2023 Spiegel 312 | * 313 | * Licensed under the Apache License, Version 2.0 (the "License"); 314 | * you may not use this file except in compliance with the License. 315 | * You may obtain a copy of the License at 316 | * 317 | * http://www.apache.org/licenses/LICENSE-2.0 318 | * 319 | * Unless required by applicable law or agreed to in writing, software 320 | * distributed under the License is distributed on an "AS IS" BASIS, 321 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 322 | * See the License for the specific language governing permissions and 323 | * limitations under the License. 324 | */ 325 | -------------------------------------------------------------------------------- /errs_test.go: -------------------------------------------------------------------------------- 1 | package errs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "syscall" 13 | "testing" 14 | ) 15 | 16 | type testError struct { 17 | Msg string 18 | Err error 19 | } 20 | 21 | func (t *testError) Error() string { 22 | return strings.Join([]string{t.Msg, t.Err.Error()}, ": ") 23 | } 24 | func (t *testError) Unwrap() error { 25 | return t.Err 26 | } 27 | func (t *testError) MarshalJSON() ([]byte, error) { 28 | if t == nil { 29 | return []byte("null"), nil 30 | } 31 | elms := []string{} 32 | elms = append(elms, fmt.Sprintf(`"Type":%q`, fmt.Sprintf("%T", t))) 33 | msgBuf := &bytes.Buffer{} 34 | json.HTMLEscape(msgBuf, []byte(fmt.Sprintf(`"Msg":%q`, t.Error()))) 35 | elms = append(elms, msgBuf.String()) 36 | if t.Err != nil && !reflect.ValueOf(t.Err).IsZero() { 37 | elms = append(elms, fmt.Sprintf(`"Err":%s`, EncodeJSON(t.Err))) 38 | } 39 | 40 | return []byte("{" + strings.Join(elms, ",") + "}"), nil 41 | } 42 | 43 | var ( 44 | nilErr = New("") // nil object 45 | nilValueErr = (*Error)(nil) 46 | errTest = New("\"Error\" for test") 47 | wrapedErrTest = Wrap(errTest) 48 | wrapedErrTest2 = &testError{Msg: "test for testError", Err: wrapedErrTest} 49 | ) 50 | 51 | func TestNil(t *testing.T) { 52 | testCases := []struct { 53 | err error 54 | typeStr string 55 | ptr string 56 | msg string 57 | detail string 58 | json string 59 | badStr string 60 | }{ 61 | { 62 | err: nilErr, 63 | typeStr: "", 64 | ptr: "%!p()", 65 | msg: "", 66 | detail: ``, 67 | json: ``, 68 | badStr: `%!d()`, 69 | }, 70 | { 71 | err: nilValueErr, 72 | typeStr: "*errs.Error", 73 | ptr: "0x0", 74 | msg: "", 75 | detail: ``, 76 | json: `null`, 77 | badStr: `%!d()`, 78 | }, 79 | } 80 | 81 | for _, tc := range testCases { 82 | str := fmt.Sprintf("%T", tc.err) 83 | if str != tc.typeStr { 84 | t.Errorf("Type of Wrap(\"%v\") is %v, want %v", tc.err, str, tc.typeStr) 85 | } 86 | str = fmt.Sprintf("%p", tc.err) 87 | if str != tc.ptr { 88 | t.Errorf("Pointer of Wrap(\"%v\") is %v, want %v", tc.err, str, tc.ptr) 89 | } 90 | str = fmt.Sprintf("%v", tc.err) 91 | if str != tc.msg { 92 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.msg) 93 | } 94 | str = fmt.Sprintf("%#v", tc.err) 95 | if str != tc.detail { 96 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.detail) 97 | } 98 | str = fmt.Sprintf("%+v", tc.err) 99 | if str != tc.json { 100 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.json) 101 | } 102 | str = fmt.Sprintf("%d", tc.err) 103 | if str != tc.badStr { 104 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.badStr) 105 | } 106 | } 107 | } 108 | 109 | func TestNewWithCause(t *testing.T) { 110 | testCases := []struct { 111 | err error 112 | typeStr string 113 | ptr string 114 | msg string 115 | detail string 116 | json string 117 | badStr string 118 | }{ 119 | { 120 | err: nilErr, 121 | typeStr: "*errs.Error", 122 | ptr: "0x0", 123 | msg: "wrapped message", 124 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}}`, 125 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestNewWithCause","num":1}}`, 126 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}})`, 127 | }, 128 | { 129 | err: nilValueErr, 130 | typeStr: "*errs.Error", 131 | ptr: "0x0", 132 | msg: "wrapped message: ", 133 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}}`, 134 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestNewWithCause","num":1}}`, 135 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}})`, 136 | }, 137 | { 138 | err: os.ErrInvalid, 139 | typeStr: "*errs.Error", 140 | ptr: "0x0", 141 | msg: "wrapped message: invalid argument", 142 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errors.errorString{s:"invalid argument"}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}}`, 143 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestNewWithCause","num":1},"Cause":{"Type":"*errors.errorString","Msg":"invalid argument"}}`, 144 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errors.errorString{s:"invalid argument"}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}})`, 145 | }, 146 | { 147 | err: errTest, 148 | typeStr: "*errs.Error", 149 | ptr: "0x0", 150 | msg: "wrapped message: \"Error\" for test", 151 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}}`, 152 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestNewWithCause","num":1},"Cause":{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"\"Error\" for test"},"Context":{"function":"github.com/goark/errs.init"}}}`, 153 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}})`, 154 | }, 155 | { 156 | err: wrapedErrTest, 157 | typeStr: "*errs.Error", 158 | ptr: "0x0", 159 | msg: "wrapped message: \"Error\" for test", 160 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}}`, 161 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestNewWithCause","num":1},"Cause":{"Type":"*errs.Error","Err":{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"\"Error\" for test"},"Context":{"function":"github.com/goark/errs.init"}},"Context":{"function":"github.com/goark/errs.init"}}}`, 162 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}})`, 163 | }, 164 | { 165 | err: wrapedErrTest2, 166 | typeStr: "*errs.Error", 167 | ptr: "0x0", 168 | msg: "wrapped message: test for testError: \"Error\" for test", 169 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errs.testError{Msg:"test for testError", Err:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}}`, 170 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestNewWithCause","num":1},"Cause":{"Type":"*errs.testError","Msg":"test for testError: \"Error\" for test","Err":{"Type":"*errs.Error","Err":{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"\"Error\" for test"},"Context":{"function":"github.com/goark/errs.init"}},"Context":{"function":"github.com/goark/errs.init"}}}}`, 171 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errs.testError{Msg:"test for testError", Err:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestNewWithCause", "num":1}})`, 172 | }, 173 | } 174 | 175 | for _, tc := range testCases { 176 | err := New("wrapped message", WithCause(tc.err), WithContext("foo", "bar"), WithContext("num", 1)) 177 | str := fmt.Sprintf("%T", err) 178 | if str != tc.typeStr { 179 | t.Errorf("Type of Wrap(\"%v\") is %v, want %v", tc.err, str, tc.typeStr) 180 | } 181 | str = fmt.Sprintf("%p", err) 182 | if str == tc.ptr { 183 | t.Errorf("Pointer of Wrap(\"%v\") is %v, not want %v", tc.err, str, tc.ptr) 184 | } else { 185 | fmt.Println("Info:", str) 186 | } 187 | str = fmt.Sprintf("%s", err) 188 | if str != tc.msg { 189 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.msg) 190 | } 191 | str = fmt.Sprintf("%v", err) 192 | if str != tc.msg { 193 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.msg) 194 | } 195 | str = fmt.Sprintf("%#v", err) 196 | if str != tc.detail { 197 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.detail) 198 | } 199 | str = fmt.Sprintf("%+v", err) 200 | if str != tc.json { 201 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.json) 202 | } 203 | str = fmt.Sprintf("%d", err) 204 | if str != tc.badStr { 205 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.badStr) 206 | } 207 | if err != nil { 208 | b, e := json.Marshal(err) 209 | if e != nil { 210 | t.Errorf("json.Marshal(\"%v\") is %v, want ", tc.err, e) 211 | } else if string(b) != tc.json { 212 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, string(b), tc.json) 213 | } 214 | str = EncodeJSON(err) 215 | if str != tc.json { 216 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.json) 217 | } 218 | } 219 | } 220 | } 221 | 222 | func TestWrapWithCause(t *testing.T) { 223 | testCases := []struct { 224 | err error 225 | typeStr string 226 | ptr string 227 | msg string 228 | detail string 229 | json string 230 | badStr string 231 | }{ 232 | { 233 | err: nilErr, 234 | typeStr: "*errs.Error", 235 | ptr: "0x0", 236 | msg: "wrapped message", 237 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}}`, 238 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestWrapWithCause","num":1}}`, 239 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}})`, 240 | }, 241 | { 242 | err: nilValueErr, 243 | typeStr: "*errs.Error", 244 | ptr: "0x0", 245 | msg: "wrapped message: ", 246 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}}`, 247 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestWrapWithCause","num":1}}`, 248 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}})`, 249 | }, 250 | { 251 | err: os.ErrInvalid, 252 | typeStr: "*errs.Error", 253 | ptr: "0x0", 254 | msg: "wrapped message: invalid argument", 255 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errors.errorString{s:"invalid argument"}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}}`, 256 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestWrapWithCause","num":1},"Cause":{"Type":"*errors.errorString","Msg":"invalid argument"}}`, 257 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errors.errorString{s:"invalid argument"}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}})`, 258 | }, 259 | { 260 | err: errTest, 261 | typeStr: "*errs.Error", 262 | ptr: "0x0", 263 | msg: "wrapped message: \"Error\" for test", 264 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}}`, 265 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestWrapWithCause","num":1},"Cause":{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"\"Error\" for test"},"Context":{"function":"github.com/goark/errs.init"}}}`, 266 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}})`, 267 | }, 268 | { 269 | err: wrapedErrTest, 270 | typeStr: "*errs.Error", 271 | ptr: "0x0", 272 | msg: "wrapped message: \"Error\" for test", 273 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}}`, 274 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestWrapWithCause","num":1},"Cause":{"Type":"*errs.Error","Err":{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"\"Error\" for test"},"Context":{"function":"github.com/goark/errs.init"}},"Context":{"function":"github.com/goark/errs.init"}}}`, 275 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}})`, 276 | }, 277 | { 278 | err: wrapedErrTest2, 279 | typeStr: "*errs.Error", 280 | ptr: "0x0", 281 | msg: "wrapped message: test for testError: \"Error\" for test", 282 | detail: `*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errs.testError{Msg:"test for testError", Err:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}}`, 283 | json: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapped message"},"Context":{"foo":"bar","function":"github.com/goark/errs.TestWrapWithCause","num":1},"Cause":{"Type":"*errs.testError","Msg":"test for testError: \"Error\" for test","Err":{"Type":"*errs.Error","Err":{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"\"Error\" for test"},"Context":{"function":"github.com/goark/errs.init"}},"Context":{"function":"github.com/goark/errs.init"}}}}`, 284 | badStr: `%!d(*errs.Error{Err:&errors.errorString{s:"wrapped message"}, Cause:&errs.testError{Msg:"test for testError", Err:*errs.Error{Err:*errs.Error{Err:&errors.errorString{s:"\"Error\" for test"}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}, Cause:, Context:map[string]interface {}{"function":"github.com/goark/errs.init"}}}, Context:map[string]interface {}{"foo":"bar", "function":"github.com/goark/errs.TestWrapWithCause", "num":1}})`, 285 | }, 286 | } 287 | 288 | for _, tc := range testCases { 289 | err := Wrap(errors.New("wrapped message"), WithCause(tc.err), WithContext("foo", "bar"), WithContext("num", 1)) 290 | str := fmt.Sprintf("%T", err) 291 | if str != tc.typeStr { 292 | t.Errorf("Type of Wrap(\"%v\") is %v, want %v", tc.err, str, tc.typeStr) 293 | } 294 | str = fmt.Sprintf("%p", err) 295 | if str == tc.ptr { 296 | t.Errorf("Pointer of Wrap(\"%v\") is %v, not want %v", tc.err, str, tc.ptr) 297 | } else { 298 | fmt.Println("Info:", str) 299 | } 300 | str = fmt.Sprintf("%v", err) 301 | if str != tc.msg { 302 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.msg) 303 | } 304 | str = fmt.Sprintf("%#v", err) 305 | if str != tc.detail { 306 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.detail) 307 | } 308 | str = fmt.Sprintf("%+v", err) 309 | if str != tc.json { 310 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.json) 311 | } 312 | str = fmt.Sprintf("%d", err) 313 | if str != tc.badStr { 314 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.badStr) 315 | } 316 | if err != nil { 317 | b, e := json.Marshal(err) 318 | if e != nil { 319 | t.Errorf("json.Marshal(\"%v\") is %v, want ", tc.err, e) 320 | } else if string(b) != tc.json { 321 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, string(b), tc.json) 322 | } 323 | str = EncodeJSON(err) 324 | if str != tc.json { 325 | t.Errorf("Wrap(\"%v\") is %v, want %v", tc.err, str, tc.json) 326 | } 327 | } 328 | } 329 | } 330 | 331 | func TestMultiError(t *testing.T) { 332 | testCases := []struct { 333 | err error 334 | want string 335 | }{ 336 | {err: Wrap(os.ErrInvalid, WithCause(errors.Join(io.EOF, io.ErrUnexpectedEOF))), want: `{"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"invalid argument"},"Context":{"function":"github.com/goark/errs.TestMultiError"},"Cause":{"Type":"*errors.joinError","Msg":"EOF\nunexpected EOF","Cause":[{"Type":"*errors.errorString","Msg":"EOF"},{"Type":"*errors.errorString","Msg":"unexpected EOF"}]}}`}, 337 | {err: Wrap(errors.Join(io.EOF, io.ErrUnexpectedEOF), WithCause(os.ErrInvalid)), want: `{"Type":"*errs.Error","Err":{"Type":"*errors.joinError","Msg":"EOF\nunexpected EOF","Cause":[{"Type":"*errors.errorString","Msg":"EOF"},{"Type":"*errors.errorString","Msg":"unexpected EOF"}]},"Context":{"function":"github.com/goark/errs.TestMultiError"},"Cause":{"Type":"*errors.errorString","Msg":"invalid argument"}}`}, 338 | } 339 | 340 | for _, tc := range testCases { 341 | if got := EncodeJSON(tc.err); got != tc.want { 342 | t.Errorf("result EncodeJSON(\"%v\") is %v, want %v", tc.err, got, tc.want) 343 | } 344 | } 345 | } 346 | 347 | func TestIs(t *testing.T) { 348 | testCases := []struct { 349 | err error 350 | res bool 351 | target error 352 | }{ 353 | {err: nil, res: true, target: nil}, 354 | {err: New("error"), res: false, target: nil}, 355 | {err: New(""), res: true, target: nil}, 356 | {err: Wrap(nil), res: true, target: nil}, 357 | {err: nil, res: false, target: errTest}, 358 | {err: errTest, res: false, target: nil}, 359 | {err: errTest, res: true, target: errTest}, 360 | {err: errTest, res: false, target: os.ErrInvalid}, 361 | {err: New("wrapped error", WithCause(os.ErrInvalid)), res: true, target: os.ErrInvalid}, 362 | {err: New("wrapped error", WithCause(os.ErrInvalid)), res: false, target: errTest}, 363 | {err: New("wrapped error", WithCause(errTest)), res: true, target: errTest}, 364 | {err: New("wrapped error", WithCause(errTest)), res: false, target: os.ErrInvalid}, 365 | {err: New("wrapped error", WithCause(errors.Join(io.EOF, os.ErrInvalid))), res: true, target: os.ErrInvalid}, 366 | {err: Wrap(errors.Join(io.EOF, os.ErrInvalid)), res: true, target: os.ErrInvalid}, 367 | } 368 | 369 | for _, tc := range testCases { 370 | if ok := Is(tc.err, tc.target); ok != tc.res { 371 | t.Errorf("result Is(\"%v\", \"%v\") is %v, want %v", tc.err, tc.target, ok, tc.res) 372 | } 373 | } 374 | } 375 | 376 | func TestAs(t *testing.T) { 377 | testCases := []struct { 378 | err error 379 | res bool 380 | cause error 381 | }{ 382 | {err: nil, res: false, cause: nil}, 383 | {err: New("wrapped error", WithCause(syscall.ENOENT)), res: true, cause: syscall.ENOENT}, 384 | {err: New("wrapped error", WithCause(errors.Join(syscall.ENOENT, os.ErrInvalid))), res: true, cause: syscall.ENOENT}, 385 | {err: Wrap(errors.Join(syscall.ENOENT, os.ErrInvalid)), res: true, cause: syscall.ENOENT}, 386 | } 387 | 388 | for _, tc := range testCases { 389 | var cs syscall.Errno 390 | if ok := As(tc.err, &cs); ok != tc.res { 391 | t.Errorf("result if As(\"%v\") is %v, want %v", tc.err, ok, tc.res) 392 | if ok && cs != tc.cause { 393 | t.Errorf("As(\"%v\") = \"%v\", want \"%v\"", tc.err, cs, tc.cause) 394 | } 395 | } 396 | } 397 | } 398 | 399 | func TestUnwrap(t *testing.T) { 400 | testCases := []struct { 401 | err error 402 | unwrap string 403 | cause error 404 | }{ 405 | {err: nil, unwrap: "", cause: nil}, 406 | {err: syscall.ENOENT, unwrap: "", cause: nil}, 407 | {err: New("wrapped error"), unwrap: "", cause: nil}, 408 | {err: New("wrapped error", WithCause(syscall.ENOENT)), unwrap: "no such file or directory", cause: syscall.ENOENT}, 409 | {err: Wrap(syscall.ENOENT), unwrap: "no such file or directory", cause: syscall.ENOENT}, 410 | } 411 | 412 | for _, tc := range testCases { 413 | cs := Unwrap(tc.err) 414 | if cs != nil { 415 | str := cs.Error() 416 | if str != tc.unwrap { 417 | t.Errorf("Unwrap(\"%v\") = \"%v\", want \"%v\"", tc.err, str, tc.unwrap) 418 | } 419 | } 420 | if cs != tc.cause { 421 | t.Errorf("As(\"%v\") = \"%v\", want \"%v\"", tc.err, cs, tc.cause) 422 | } 423 | } 424 | } 425 | 426 | func TestUnwraps(t *testing.T) { 427 | testCases := []struct { 428 | err error 429 | unwraped []error 430 | }{ 431 | {err: nil, unwraped: nil}, 432 | {err: os.ErrInvalid, unwraped: nil}, 433 | {err: Wrap(os.ErrInvalid), unwraped: []error{os.ErrInvalid}}, 434 | {err: errors.Join(syscall.ENOENT, os.ErrInvalid), unwraped: []error{syscall.ENOENT, os.ErrInvalid}}, 435 | } 436 | 437 | for _, tc := range testCases { 438 | got := Unwraps(tc.err) 439 | if len(got) != len(tc.unwraped) { 440 | t.Errorf("Unwrap(\"%v\") = \"%v\", want \"%v\"", tc.err, got, tc.unwraped) 441 | } 442 | for i, e := range tc.unwraped { 443 | if e != got[i] { 444 | t.Errorf("Unwrap(\"%v\") = \"%v\", want \"%v\"", tc.err, got, tc.unwraped) 445 | } 446 | } 447 | } 448 | } 449 | 450 | /* Copyright 2019-2023 Spiegel 451 | * 452 | * Licensed under the Apache License, Version 2.0 (the "License"); 453 | * you may not use this file except in compliance with the License. 454 | * You may obtain a copy of the License at 455 | * 456 | * http://www.apache.org/licenses/LICENSE-2.0 457 | * 458 | * Unless required by applicable law or agreed to in writing, software 459 | * distributed under the License is distributed on an "AS IS" BASIS, 460 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 461 | * See the License for the specific language governing permissions and 462 | * limitations under the License. 463 | */ 464 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package errs_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "sync" 8 | 9 | "github.com/goark/errs" 10 | ) 11 | 12 | func ExampleNew() { 13 | err := errs.New( 14 | "wrapper error", 15 | errs.WithCause(os.ErrInvalid), 16 | errs.WithContext("foo", "bar"), 17 | ) 18 | fmt.Printf("%+v", err) 19 | // Output: 20 | // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"wrapper error"},"Context":{"foo":"bar","function":"github.com/goark/errs_test.ExampleNew"},"Cause":{"Type":"*errors.errorString","Msg":"invalid argument"}} 21 | } 22 | 23 | func ExampleError() { 24 | err := errs.Wrap( 25 | os.ErrInvalid, 26 | errs.WithContext("foo1", "bar1"), 27 | ) 28 | _ = err.(*errs.Error).SetContext("foo2", "bar2") 29 | fmt.Printf("%+v", err) 30 | // Output: 31 | // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"invalid argument"},"Context":{"foo1":"bar1","foo2":"bar2","function":"github.com/goark/errs_test.ExampleError"}} 32 | } 33 | 34 | func ExampleEncodeJSON() { 35 | _, err := os.Open("not-exist.txt") 36 | fmt.Printf("%v", errs.EncodeJSON(err)) 37 | // Output: 38 | // {"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}} 39 | } 40 | 41 | func ExampleJoin() { 42 | err := errs.Join(errors.New("error 1"), errors.New("error 2")) 43 | fmt.Println(err) 44 | errlist, ok := err.(*errs.Errors) 45 | if !ok { 46 | return 47 | } 48 | errlist.Add(errors.New("error 3")) 49 | fmt.Printf("%+v\n", errlist.ErrorOrNil()) 50 | // Output: 51 | // error 1 52 | // error 2 53 | // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 1"},{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 3"}]} 54 | } 55 | 56 | func ExampleErrors() { 57 | errlist := &errs.Errors{} 58 | var wg sync.WaitGroup 59 | for i := 1; i <= 100000; i++ { 60 | i := i 61 | wg.Add(1) 62 | go func() { 63 | defer wg.Done() 64 | errlist.Add(fmt.Errorf("error %d", i)) 65 | }() 66 | } 67 | wg.Wait() 68 | fmt.Println("error ount =", len(errlist.Unwrap())) 69 | // Output: 70 | // error ount = 100000 71 | } 72 | 73 | /* Copyright 2019-2023 Spiegel 74 | * 75 | * Licensed under the Apache License, Version 2.0 (the "License"); 76 | * you may not use this file except in compliance with the License. 77 | * You may obtain a copy of the License at 78 | * 79 | * http://www.apache.org/licenses/LICENSE-2.0 80 | * 81 | * Unless required by applicable law or agreed to in writing, software 82 | * distributed under the License is distributed on an "AS IS" BASIS, 83 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 84 | * See the License for the specific language governing permissions and 85 | * limitations under the License. 86 | */ 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goark/errs 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.20 2 | 3 | use ( 4 | . 5 | zapobject 6 | ) 7 | -------------------------------------------------------------------------------- /sample/sample1.go: -------------------------------------------------------------------------------- 1 | //go:build run 2 | // +build run 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/goark/errs" 11 | ) 12 | 13 | func checkFileOpen(path string) error { 14 | file, err := os.Open(path) 15 | if err != nil { 16 | return errs.New( 17 | "file open error", 18 | errs.WithCause(err), 19 | errs.WithContext("path", path), 20 | ) 21 | } 22 | defer file.Close() 23 | 24 | return nil 25 | } 26 | 27 | func main() { 28 | if err := checkFileOpen("not-exist.txt"); err != nil { 29 | fmt.Printf("%v\n", err) // file open error: open not-exist.txt: no such file or directory 30 | fmt.Printf("%#v\n", err) // *errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}} 31 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"file open error"},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"},"Cause":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}}} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample/sample2.go: -------------------------------------------------------------------------------- 1 | //go:build run 2 | // +build run 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/goark/errs" 11 | ) 12 | 13 | func checkFileOpen(path string) error { 14 | file, err := os.Open(path) 15 | if err != nil { 16 | return errs.Wrap( 17 | err, 18 | errs.WithContext("path", path), 19 | ) 20 | } 21 | defer file.Close() 22 | 23 | return nil 24 | } 25 | 26 | func main() { 27 | if err := checkFileOpen("not-exist.txt"); err != nil { 28 | fmt.Printf("%v\n", err) // open not-exist.txt: no such file or directory 29 | fmt.Printf("%#v\n", err) // *errs.Error{Err:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Cause:, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}} 30 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"}} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample/sample3.go: -------------------------------------------------------------------------------- 1 | //go:build run 2 | // +build run 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/goark/errs" 12 | ) 13 | 14 | func checkFileOpen(path string) error { 15 | file, err := os.Open(path) 16 | if err != nil { 17 | return errs.Wrap( 18 | errors.New("file open error"), 19 | errs.WithCause(err), 20 | errs.WithContext("path", path), 21 | ) 22 | } 23 | defer file.Close() 24 | 25 | return nil 26 | } 27 | 28 | func main() { 29 | if err := checkFileOpen("not-exist.txt"); err != nil { 30 | fmt.Printf("%v\n", err) // file open error: open not-exist.txt: no such file or directory 31 | fmt.Printf("%#v\n", err) // *errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}} 32 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"file open error"},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"},"Cause":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}}} 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample/sample4.go: -------------------------------------------------------------------------------- 1 | //go:build run 2 | // +build run 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "github.com/goark/errs" 13 | ) 14 | 15 | func generateMultiError() error { 16 | return errs.New("error with multiple causes", errs.WithCause(errors.Join(os.ErrInvalid, io.EOF))) 17 | } 18 | 19 | func main() { 20 | err := generateMultiError() 21 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"error with multiple causes"},"Context":{"function":"main.generateMultiError"},"Cause":{"Type":"*errors.joinError","Msg":"invalid argument\nEOF","Cause":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]}} 22 | fmt.Println(errors.Is(err, io.EOF)) // true 23 | } 24 | -------------------------------------------------------------------------------- /sample/sample5.go: -------------------------------------------------------------------------------- 1 | //go:build run 2 | // +build run 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "github.com/goark/errs" 13 | ) 14 | 15 | func generateMultiError() error { 16 | return errs.Join(os.ErrInvalid, io.EOF) 17 | } 18 | 19 | func main() { 20 | err := generateMultiError() 21 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]} 22 | fmt.Println(errors.Is(err, io.EOF)) // true 23 | } 24 | -------------------------------------------------------------------------------- /sample/sample6.go: -------------------------------------------------------------------------------- 1 | //go:build run 2 | // +build run 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | 10 | "github.com/goark/errs" 11 | ) 12 | 13 | func generateMultiError() error { 14 | errlist := &errs.Errors{} 15 | var wg sync.WaitGroup 16 | for i := 1; i <= 2; i++ { 17 | i := i 18 | wg.Add(1) 19 | go func() { 20 | defer wg.Done() 21 | errlist.Add(fmt.Errorf("error %d", i)) 22 | }() 23 | } 24 | wg.Wait() 25 | return errlist.ErrorOrNil() 26 | } 27 | 28 | func main() { 29 | err := generateMultiError() 30 | fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 1"}]} 31 | } 32 | -------------------------------------------------------------------------------- /zapobject/example_test.go: -------------------------------------------------------------------------------- 1 | package zapobject_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/goark/errs" 9 | "github.com/goark/errs/zapobject" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func checkFileOpen(path string) error { 14 | file, err := os.Open(path) 15 | if err != nil { 16 | return errs.New( 17 | "file open error", 18 | errs.WithCause(err), 19 | errs.WithContext("path", path), 20 | ) 21 | } 22 | defer file.Close() 23 | 24 | return nil 25 | } 26 | 27 | func generateMultiError() error { 28 | errlist := &errs.Errors{} 29 | var wg sync.WaitGroup 30 | for i := 1; i <= 2; i++ { 31 | i := i 32 | wg.Add(1) 33 | go func() { 34 | defer wg.Done() 35 | errlist.Add(fmt.Errorf("error %d", i)) 36 | }() 37 | } 38 | wg.Wait() 39 | return errlist.ErrorOrNil() 40 | } 41 | 42 | func Example() { 43 | logger := zap.NewExample() 44 | defer logger.Sync() 45 | 46 | if err := checkFileOpen("not-exist.txt"); err != nil { 47 | logger.Error("err", zap.Object("error", zapobject.New(err))) 48 | } 49 | if err := generateMultiError(); err != nil { 50 | logger.Error("err", zap.Object("error", zapobject.New(err))) 51 | } 52 | // Output: 53 | // {"level":"error","msg":"err","error":{"type":"*errs.Error","msg":"file open error: open not-exist.txt: no such file or directory","error":{"type":"*errors.errorString","msg":"file open error"},"cause":{"type":"*fs.PathError","msg":"open not-exist.txt: no such file or directory","cause":{"type":"syscall.Errno","msg":"no such file or directory"}},"context":{"function":"github.com/goark/errs/zapobject_test.checkFileOpen","path":"not-exist.txt"}}} 54 | // {"level":"error","msg":"err","error":{"type":"*errs.Errors","msg":"error 2\nerror 1","causes":[{"type":"*errors.errorString","msg":"error 2"},{"type":"*errors.errorString","msg":"error 1"}]}} 55 | } 56 | -------------------------------------------------------------------------------- /zapobject/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goark/errs/zapobject 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/goark/errs v1.3.0 7 | go.uber.org/zap v1.24.0 8 | ) 9 | 10 | require ( 11 | go.uber.org/atomic v1.7.0 // indirect 12 | go.uber.org/multierr v1.6.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /zapobject/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/goark/errs v1.3.0 h1:faiMaXCIgCt98Vmn9PGyFp7XL+zHqEK0WfBGRT1/Yz4= 6 | github.com/goark/errs v1.3.0/go.mod h1:ZsQucxaDFVfSB8I99j4bxkDRfNOrlKINwg72QMuRWKw= 7 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 12 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 13 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 14 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 15 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 16 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 17 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 18 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 19 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | -------------------------------------------------------------------------------- /zapobject/zapobject.go: -------------------------------------------------------------------------------- 1 | package zapobject 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/goark/errs" 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | type ErrObject struct { 13 | Err error 14 | } 15 | 16 | // New wrapped error instance in ErrObject. 17 | func New(err error) ErrObject { 18 | return ErrObject{Err: err} 19 | } 20 | 21 | // MarshalLogObject method is object marshaler for go.uber.org/zap. 22 | func (e ErrObject) MarshalLogObject(enc zapcore.ObjectEncoder) error { 23 | if e.Err == nil { 24 | return nil 25 | } 26 | var ee *errs.Error 27 | if errs.As(e.Err, &ee) { 28 | enc.AddString("type", fmt.Sprintf("%T", ee)) 29 | enc.AddString("msg", ee.Error()) 30 | if ee.Err != nil { 31 | if err := enc.AddObject("error", New(ee.Err)); err != nil { 32 | return err 33 | } 34 | } 35 | if ee.Cause != nil { 36 | if err := enc.AddObject("cause", New(ee.Cause)); err != nil { 37 | return err 38 | } 39 | } 40 | if len(ee.Context) > 0 { 41 | keys := make([]string, 0, len(ee.Context)) 42 | for k := range ee.Context { 43 | keys = append(keys, k) 44 | } 45 | sort.Strings(keys) 46 | enc.OpenNamespace("context") 47 | for _, k := range keys { 48 | _ = enc.AddReflected(k, ee.Context[k]) 49 | } 50 | } 51 | } else { 52 | enc.AddString("type", fmt.Sprintf("%T", e.Err)) 53 | enc.AddString("msg", e.Err.Error()) 54 | if errList := errs.Unwraps(e.Err); len(errList) > 0 { 55 | if len(errList) == 1 { 56 | return enc.AddObject("cause", New(errList[0])) 57 | } 58 | elist := make([]ErrObject, 0, len(errList)) 59 | for _, e := range errList { 60 | elist = append(elist, New(e)) 61 | } 62 | zap.Objects("causes", elist).AddTo(enc) 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | /* Copyright 2023 Spiegel 69 | * 70 | * Licensed under the Apache License, Version 2.0 (the "License"); 71 | * you may not use this file except in compliance with the License. 72 | * You may obtain a copy of the License at 73 | * 74 | * http://www.apache.org/licenses/LICENSE-2.0 75 | * 76 | * Unless required by applicable law or agreed to in writing, software 77 | * distributed under the License is distributed on an "AS IS" BASIS, 78 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 79 | * See the License for the specific language governing permissions and 80 | * limitations under the License. 81 | */ 82 | --------------------------------------------------------------------------------