├── .github
└── workflows
│ └── CI.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── README.md
├── backoff.go
├── backoff_test.go
├── client.go
├── client_test.go
├── docs
└── ravenTree-logo.png
├── err_collections.go
├── go.mod
├── go.sum
├── options.go
└── wrapper.go
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: Quality
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | name: Test with Coverage
6 | runs-on: ubuntu-latest
7 |
8 | steps:
9 | - name: Install Go
10 | uses: actions/setup-go@v3
11 | with:
12 | go-version: '1.22'
13 |
14 | - name: Checkout code
15 | uses: actions/checkout@v3
16 |
17 | - name: Install golangci-lint
18 | run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
19 |
20 | - name: Run golangci-lint
21 | run: golangci-lint run ./...
22 |
23 | - name: Run Unit tests
24 | run: |
25 | go test -race -covermode atomic -coverprofile=covprofile ./...
26 |
27 | - name: Install goveralls
28 | run: go install github.com/mattn/goveralls@latest
29 |
30 | - name: Send coverage
31 | uses: shogo82148/actions-goveralls@v1
32 | with:
33 | path-to-profile: covprofile
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/go,goland,goland+all,goland+iml,intellij,intellij+all,intellij+iml,windows,macos,linux
2 | # Edit at https://www.toptal.com/developers/gitignore\?templates\=go,goland,goland+all,goland+iml,intellij,intellij+all,intellij+iml,windows,macos,linux
3 |
4 | ### Go ###
5 | # If you prefer the allow list template instead of the deny list, see community template:
6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
7 | #
8 | # Binaries for programs and plugins
9 | *.exe
10 | *.exe~
11 | *.dll
12 | *.so
13 | *.dylib
14 |
15 | # Test binary, built with `go test -c`
16 | *.test
17 |
18 | # Output of the go coverage tool, specifically when used with LiteIDE
19 | *.out
20 |
21 | # Dependency directories (remove the comment below to include it)
22 | # vendor/
23 |
24 | # Go workspace file
25 | go.work
26 |
27 | ### GoLand ###
28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
30 |
31 | # User-specific stuff
32 | .idea/**/workspace.xml
33 | .idea/**/tasks.xml
34 | .idea/**/usage.statistics.xml
35 | .idea/**/dictionaries
36 | .idea/**/shelf
37 |
38 | # AWS User-specific
39 | .idea/**/aws.xml
40 |
41 | # Generated files
42 | .idea/**/contentModel.xml
43 |
44 | # Sensitive or high-churn files
45 | .idea/**/dataSources/
46 | .idea/**/dataSources.ids
47 | .idea/**/dataSources.local.xml
48 | .idea/**/sqlDataSources.xml
49 | .idea/**/dynamic.xml
50 | .idea/**/uiDesigner.xml
51 | .idea/**/dbnavigator.xml
52 |
53 | # Gradle
54 | .idea/**/gradle.xml
55 | .idea/**/libraries
56 |
57 | # Gradle and Maven with auto-import
58 | # When using Gradle or Maven with auto-import, you should exclude module files,
59 | # since they will be recreated, and may cause churn. Uncomment if using
60 | # auto-import.
61 | # .idea/artifacts
62 | # .idea/compiler.xml
63 | # .idea/jarRepositories.xml
64 | # .idea/modules.xml
65 | # .idea/*.iml
66 | # .idea/modules
67 | # *.iml
68 | # *.ipr
69 |
70 | # CMake
71 | cmake-build-*/
72 |
73 | # Mongo Explorer plugin
74 | .idea/**/mongoSettings.xml
75 |
76 | # File-based project format
77 | *.iws
78 |
79 | # IntelliJ
80 | out/
81 |
82 | # mpeltonen/sbt-idea plugin
83 | .idea_modules/
84 |
85 | # JIRA plugin
86 | atlassian-ide-plugin.xml
87 |
88 | # Cursive Clojure plugin
89 | .idea/replstate.xml
90 |
91 | # SonarLint plugin
92 | .idea/sonarlint/
93 |
94 | # Crashlytics plugin (for Android Studio and IntelliJ)
95 | com_crashlytics_export_strings.xml
96 | crashlytics.properties
97 | crashlytics-build.properties
98 | fabric.properties
99 |
100 | # Editor-based Rest Client
101 | .idea/httpRequests
102 |
103 | # Android studio 3.1+ serialized cache file
104 | .idea/caches/build_file_checksums.ser
105 |
106 | ### GoLand Patch ###
107 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186\#issuecomment-215987721
108 |
109 | # *.iml
110 | # modules.xml
111 | # .idea/misc.xml
112 | # *.ipr
113 |
114 | # Sonarlint plugin
115 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
116 | .idea/**/sonarlint/
117 |
118 | # SonarQube Plugin
119 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
120 | .idea/**/sonarIssues.xml
121 |
122 | # Markdown Navigator plugin
123 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
124 | .idea/**/markdown-navigator.xml
125 | .idea/**/markdown-navigator-enh.xml
126 | .idea/**/markdown-navigator/
127 |
128 | # Cache file creation bug
129 | # See https://youtrack.jetbrains.com/issue/JBR-2257
130 | .idea/$CACHE_FILE$
131 |
132 | # CodeStream plugin
133 | # https://plugins.jetbrains.com/plugin/12206-codestream
134 | .idea/codestream.xml
135 |
136 | # Azure Toolkit for IntelliJ plugin
137 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
138 | .idea/**/azureSettings.xml
139 |
140 | ### GoLand+all ###
141 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
142 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
143 |
144 | # User-specific stuff
145 |
146 | # AWS User-specific
147 |
148 | # Generated files
149 |
150 | # Sensitive or high-churn files
151 |
152 | # Gradle
153 |
154 | # Gradle and Maven with auto-import
155 | # When using Gradle or Maven with auto-import, you should exclude module files,
156 | # since they will be recreated, and may cause churn. Uncomment if using
157 | # auto-import.
158 | # .idea/artifacts
159 | # .idea/compiler.xml
160 | # .idea/jarRepositories.xml
161 | # .idea/modules.xml
162 | # .idea/*.iml
163 | # .idea/modules
164 | # *.iml
165 | # *.ipr
166 |
167 | # CMake
168 |
169 | # Mongo Explorer plugin
170 |
171 | # File-based project format
172 |
173 | # IntelliJ
174 |
175 | # mpeltonen/sbt-idea plugin
176 |
177 | # JIRA plugin
178 |
179 | # Cursive Clojure plugin
180 |
181 | # SonarLint plugin
182 |
183 | # Crashlytics plugin (for Android Studio and IntelliJ)
184 |
185 | # Editor-based Rest Client
186 |
187 | # Android studio 3.1+ serialized cache file
188 |
189 | ### GoLand+all Patch ###
190 | # Ignore everything but code style settings and run configurations
191 | # that are supposed to be shared within teams.
192 |
193 | .idea/*
194 |
195 | !.idea/codeStyles
196 | !.idea/runConfigurations
197 |
198 | ### GoLand+iml ###
199 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
200 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
201 |
202 | # User-specific stuff
203 |
204 | # AWS User-specific
205 |
206 | # Generated files
207 |
208 | # Sensitive or high-churn files
209 |
210 | # Gradle
211 |
212 | # Gradle and Maven with auto-import
213 | # When using Gradle or Maven with auto-import, you should exclude module files,
214 | # since they will be recreated, and may cause churn. Uncomment if using
215 | # auto-import.
216 | # .idea/artifacts
217 | # .idea/compiler.xml
218 | # .idea/jarRepositories.xml
219 | # .idea/modules.xml
220 | # .idea/*.iml
221 | # .idea/modules
222 | # *.iml
223 | # *.ipr
224 |
225 | # CMake
226 |
227 | # Mongo Explorer plugin
228 |
229 | # File-based project format
230 |
231 | # IntelliJ
232 |
233 | # mpeltonen/sbt-idea plugin
234 |
235 | # JIRA plugin
236 |
237 | # Cursive Clojure plugin
238 |
239 | # SonarLint plugin
240 |
241 | # Crashlytics plugin (for Android Studio and IntelliJ)
242 |
243 | # Editor-based Rest Client
244 |
245 | # Android studio 3.1+ serialized cache file
246 |
247 | ### GoLand+iml Patch ###
248 | # Reason: https://github.com/joeblau/gitignore.io/issues/186\#issuecomment-249601023
249 |
250 | *.iml
251 | modules.xml
252 | .idea/misc.xml
253 | *.ipr
254 |
255 | ### Intellij ###
256 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
257 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
258 |
259 | # User-specific stuff
260 |
261 | # AWS User-specific
262 |
263 | # Generated files
264 |
265 | # Sensitive or high-churn files
266 |
267 | # Gradle
268 |
269 | # Gradle and Maven with auto-import
270 | # When using Gradle or Maven with auto-import, you should exclude module files,
271 | # since they will be recreated, and may cause churn. Uncomment if using
272 | # auto-import.
273 | # .idea/artifacts
274 | # .idea/compiler.xml
275 | # .idea/jarRepositories.xml
276 | # .idea/modules.xml
277 | # .idea/*.iml
278 | # .idea/modules
279 | # *.iml
280 | # *.ipr
281 |
282 | # CMake
283 |
284 | # Mongo Explorer plugin
285 |
286 | # File-based project format
287 |
288 | # IntelliJ
289 |
290 | # mpeltonen/sbt-idea plugin
291 |
292 | # JIRA plugin
293 |
294 | # Cursive Clojure plugin
295 |
296 | # SonarLint plugin
297 |
298 | # Crashlytics plugin (for Android Studio and IntelliJ)
299 |
300 | # Editor-based Rest Client
301 |
302 | # Android studio 3.1+ serialized cache file
303 |
304 | ### Intellij Patch ###
305 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186\#issuecomment-215987721
306 |
307 | # *.iml
308 | # modules.xml
309 | # .idea/misc.xml
310 | # *.ipr
311 |
312 | # Sonarlint plugin
313 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
314 |
315 | # SonarQube Plugin
316 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
317 |
318 | # Markdown Navigator plugin
319 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
320 |
321 | # Cache file creation bug
322 | # See https://youtrack.jetbrains.com/issue/JBR-2257
323 |
324 | # CodeStream plugin
325 | # https://plugins.jetbrains.com/plugin/12206-codestream
326 |
327 | # Azure Toolkit for IntelliJ plugin
328 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
329 |
330 | ### Intellij+all ###
331 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
332 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
333 |
334 | # User-specific stuff
335 |
336 | # AWS User-specific
337 |
338 | # Generated files
339 |
340 | # Sensitive or high-churn files
341 |
342 | # Gradle
343 |
344 | # Gradle and Maven with auto-import
345 | # When using Gradle or Maven with auto-import, you should exclude module files,
346 | # since they will be recreated, and may cause churn. Uncomment if using
347 | # auto-import.
348 | # .idea/artifacts
349 | # .idea/compiler.xml
350 | # .idea/jarRepositories.xml
351 | # .idea/modules.xml
352 | # .idea/*.iml
353 | # .idea/modules
354 | # *.iml
355 | # *.ipr
356 |
357 | # CMake
358 |
359 | # Mongo Explorer plugin
360 |
361 | # File-based project format
362 |
363 | # IntelliJ
364 |
365 | # mpeltonen/sbt-idea plugin
366 |
367 | # JIRA plugin
368 |
369 | # Cursive Clojure plugin
370 |
371 | # SonarLint plugin
372 |
373 | # Crashlytics plugin (for Android Studio and IntelliJ)
374 |
375 | # Editor-based Rest Client
376 |
377 | # Android studio 3.1+ serialized cache file
378 |
379 | ### Intellij+all Patch ###
380 | # Ignore everything but code style settings and run configurations
381 | # that are supposed to be shared within teams.
382 |
383 |
384 |
385 | ### Intellij+iml ###
386 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
387 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
388 |
389 | # User-specific stuff
390 |
391 | # AWS User-specific
392 |
393 | # Generated files
394 |
395 | # Sensitive or high-churn files
396 |
397 | # Gradle
398 |
399 | # Gradle and Maven with auto-import
400 | # When using Gradle or Maven with auto-import, you should exclude module files,
401 | # since they will be recreated, and may cause churn. Uncomment if using
402 | # auto-import.
403 | # .idea/artifacts
404 | # .idea/compiler.xml
405 | # .idea/jarRepositories.xml
406 | # .idea/modules.xml
407 | # .idea/*.iml
408 | # .idea/modules
409 | # *.iml
410 | # *.ipr
411 |
412 | # CMake
413 |
414 | # Mongo Explorer plugin
415 |
416 | # File-based project format
417 |
418 | # IntelliJ
419 |
420 | # mpeltonen/sbt-idea plugin
421 |
422 | # JIRA plugin
423 |
424 | # Cursive Clojure plugin
425 |
426 | # SonarLint plugin
427 |
428 | # Crashlytics plugin (for Android Studio and IntelliJ)
429 |
430 | # Editor-based Rest Client
431 |
432 | # Android studio 3.1+ serialized cache file
433 |
434 | ### Intellij+iml Patch ###
435 | # Reason: https://github.com/joeblau/gitignore.io/issues/186\#issuecomment-249601023
436 |
437 |
438 | ### Linux ###
439 | *~
440 |
441 | # temporary files which can be created if a process still has a handle open of a deleted file
442 | .fuse_hidden*
443 |
444 | # KDE directory preferences
445 | .directory
446 |
447 | # Linux trash folder which might appear on any partition or disk
448 | .Trash-*
449 |
450 | # .nfs files are created when an open file is removed but is still being accessed
451 | .nfs*
452 |
453 | ### macOS ###
454 | # General
455 | .DS_Store
456 | .AppleDouble
457 | .LSOverride
458 |
459 | # Icon must end with two
460 | Icon
461 |
462 |
463 | # Thumbnails
464 | ._*
465 |
466 | # Files that might appear in the root of a volume
467 | .DocumentRevisions-V100
468 | .fseventsd
469 | .Spotlight-V100
470 | .TemporaryItems
471 | .Trashes
472 | .VolumeIcon.icns
473 | .com.apple.timemachine.donotpresent
474 |
475 | # Directories potentially created on remote AFP share
476 | .AppleDB
477 | .AppleDesktop
478 | Network Trash Folder
479 | Temporary Items
480 | .apdisk
481 |
482 | ### macOS Patch ###
483 | # iCloud generated files
484 | *.icloud
485 |
486 | ### Windows ###
487 | # Windows thumbnail cache files
488 | Thumbs.db
489 | Thumbs.db:encryptable
490 | ehthumbs.db
491 | ehthumbs_vista.db
492 |
493 | # Dump file
494 | *.stackdump
495 |
496 | # Folder config file
497 | [Dd]esktop.ini
498 |
499 | # Recycle Bin used on file shares
500 | $RECYCLE.BIN/
501 |
502 | # Windows Installer files
503 | *.cab
504 | *.msi
505 | *.msix
506 | *.msm
507 | *.msp
508 |
509 | # Windows shortcuts
510 | *.lnk
511 |
512 | # End of https://www.toptal.com/developers/gitignore/api/go,goland,goland+all,goland+iml,intellij,intellij+all,intellij+iml,windows,macos,linux
513 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | disable-all: true
3 | enable:
4 | - unused
5 | - goconst
6 | - gofmt
7 | - goimports
8 | - mnd
9 | - gosimple
10 | - govet
11 | - unparam
12 | - nilerr
13 | - errcheck
14 | - errorlint
15 | - exhaustive
16 | - ineffassign
17 | - predeclared
18 | - typecheck
19 | - asciicheck
20 | - lll
21 | - wsl
22 | - prealloc
23 | - nestif
24 | - misspell
25 | - makezero
26 | - gocognit
27 | - varnamelen
28 | - noctx
29 | - copyloopvar
30 |
31 | linters-settings:
32 | varnamelen:
33 | min-name-length: 2
34 | ignore-decls:
35 | - c echo.Context
36 | - t testing.T
37 | - w http.ResponseWriter
38 | - r *http.Request
39 | - s strategy
40 | - b *Backoff
41 |
42 |
43 |
44 | issues:
45 | exclude-dirs:
46 | - mocks
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Andres Puello
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Raven Tree
2 |
3 | ---
4 |

5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ---
16 | Is a lightweight Go library designed to simplify HTTP requests by providing an easy-to-use interface, built-in support
17 | for various HTTP methods, accepting retry handling, and more.
18 |
19 | ## Installation
20 |
21 | To use can install it via `go get`:
22 |
23 | ```bash
24 | go get github.com/AndresXLP/ravenTree
25 | ```
26 |
27 | ---
28 |
29 | ## Usage
30 |
31 | ```go
32 | package main
33 |
34 | import (
35 | "context"
36 | "fmt"
37 | "log"
38 | "net/http"
39 | "time"
40 |
41 | "github.com/AndresXLP/ravenTree"
42 | )
43 |
44 | func main() {
45 | tree := ravenTree.NewRavensTree()
46 |
47 | options := &ravenTree.Options{
48 | Host: "http://localhost:8080",
49 | Path: "/api/resource",
50 | Method: http.MethodGet,
51 | QueryParams: map[string]string{"code": "123"},
52 | Headers: map[string]string{"Authorization": "Bearer 1234"},
53 | Timeout: 5 * time.Second,
54 | RetryCount: 3,
55 | Backoff: ravenTree.NewBackoff(
56 | ravenTree.WithStrategy(ravenTree.Exponential),
57 | ravenTree.WithBackoffDelay(3*time.Second),
58 | ravenTree.WithMaxDelay(10*time.Second),
59 | ),
60 | }
61 |
62 | resp, err := tree.SendRaven(context.Background(), options)
63 | if err != nil {
64 | log.Fatal(err)
65 | }
66 |
67 | fmt.Println(resp.ParseBodyToString())
68 | }
69 |
70 | ```
71 |
72 | ---
73 |
74 | ### Methods Provided
75 |
76 | SendRaven: This method sends an HTTP request based on the provided Options. It supports different HTTP methods such as
77 | GET, POST, PUT, DELETE, etc.
78 |
79 | ### Body Management
80 |
81 | The Body field in the Options struct can accept any type of data that can be marshaled into JSON. The library
82 | automatically handles the marshaling of the Body when sending the request.
83 |
84 | ### Headers and Query Parameters
85 |
86 | By default, the ***Content-Type*** header is set to ***application/json***.
87 |
88 | You can add additional headers and query parameters using the **Headers** and **QueryParams** fields in the Options
89 | struct.
90 |
91 | ### Timeout and Retry Options
92 |
93 | - **Timeout**: Specifies the maximum duration for a request. If the request takes longer than this duration, it will be
94 | aborted, and an error will be returned.
95 |
96 | - **RetryCount**: Specifies the number of times to retry the request if it fails. This is useful for handling transient
97 | errors or network issues. The library will automatically retry the request up to the specified number of attempts.
98 |
99 |
100 | ### Backoff Options
101 |
102 | The `Backoff` struct defines the strategy for implementing backoff delays in retry operations with the following fields:
103 |
104 | - **BackoffDelay**: Specifies the duration to wait before the next retry.
105 | - **MaxDelay**: Specifies the maximum duration for backoff delays.
106 | - **Strategy**: Determines the type of backoff (Default, Linear, or Exponential).
107 |
108 | ### Creating a New Backoff
109 |
110 | Use the `NewBackoff` function to create a new `Backoff` with optional parameters:
111 |
112 | - If no options are provided, it defaults to:
113 | - `BackoffDelay`: 0 seconds
114 | - `MaxDelay`: 10 seconds
115 | - `Strategy`: Default
116 |
117 | - Note: If `MaxDelay` is set to a value less than `BackoffDelay`, `MaxDelay` will be updated to match `BackoffDelay` to ensure
118 | valid configuration.
119 |
120 | ### Example Usage
121 |
122 | ```go
123 | package main
124 |
125 | import (
126 | "time"
127 |
128 | "github.com/AndresXLP/ravenTree"
129 | )
130 |
131 | func main() {
132 | options := &ravenTree.Options{
133 | Backoff: ravenTree.NewBackoff(
134 | WithStrategy(Linear),
135 | WithBackoffDelay(2*time.Second),
136 | WithMaxDelay(30*time.Second),
137 | ),
138 | }
139 |
140 | }
141 |
142 | ```
143 |
144 | ### Error Handling
145 |
146 | Always check for errors after calling SendRaven. If the request fails, the error will provide information about what
147 | went wrong.
148 |
149 | ### Thematic Inspiration
150 |
151 | The name **Raven Tree** reflects the connection to the mystical ravens that serve as messengers in both Game of Thrones
152 | and Norse mythology, symbolizing communication, wisdom, and the passage of information.
153 |
154 | Just as these ravens carry messages across great distances, **Raven Tree** aims to facilitate seamless communication
155 | between your application and external APIs.
156 |
157 | ---
158 |
159 | ## Authors
160 |
161 | - [@andresxlp](https://www.github.com/andresxlp)
162 |
163 | ---
164 |
165 | ### Contributing
166 |
167 | Contributions are welcome! Please open an issue or submit a pull request for any features or fixes you want to add.
168 |
169 | ## License
170 |
171 | The project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/)
172 |
--------------------------------------------------------------------------------
/backoff.go:
--------------------------------------------------------------------------------
1 | package ravenTree
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type strategy int
8 |
9 | const (
10 | // Default sets BackoffDelay to 0 seconds.
11 | // This strategy does not impose any additional delay between retries.
12 | Default strategy = iota
13 |
14 | // Lineal sets BackoffDelay to 1 second and increases the delay linearly with each retry.
15 | // For example, the delays will be 1s, 2s, 3s, etc., until MaxDelay is reached.
16 | Lineal
17 |
18 | // Exponential sets BackoffDelay to 1 second and doubles the delay with each retry.
19 | // For example, the delays will be 1s, 2s, 4s, 8s, etc., until MaxDelay is reached.
20 | Exponential
21 | )
22 |
23 | const (
24 | tenSecond = 10 * time.Second
25 | oneSecond = 1 * time.Second
26 | )
27 |
28 | // Backoff defines the strategy for implementing backoff delays in retry operations.
29 | // Fields:
30 | // - BackoffDelay: Specifies the duration to wait before the next retry.
31 | // - MaxDelay: Specifies the maximum duration to
32 | // - Strategy: Determines the type of backoff (Default, Lineal, or Exponential).
33 | type Backoff struct {
34 | BackoffDelay time.Duration
35 | MaxDelay time.Duration
36 | Strategy strategy
37 | }
38 |
39 | type BackoffOptions func(*Backoff)
40 |
41 | // NewBackoff creates a new Backoff with optional parameters.
42 | //
43 | // If no options are provided, it defaults to:
44 | // - BackoffDelay: 0 seconds
45 | // - MaxDelay: 10 seconds
46 | // - strategy: Default
47 | //
48 | // If MaxDelay is set to a value less than BackoffDelay, MaxDelay will be updated
49 | // to match BackoffDelay to ensure valid configuration.
50 | func NewBackoff(opts ...BackoffOptions) *Backoff {
51 | backoff := &Backoff{
52 | BackoffDelay: time.Duration(0),
53 | MaxDelay: tenSecond,
54 | Strategy: Default,
55 | }
56 |
57 | for _, opt := range opts {
58 | opt(backoff)
59 | }
60 |
61 | if backoff.MaxDelay < backoff.BackoffDelay {
62 | backoff.MaxDelay = backoff.BackoffDelay
63 | }
64 |
65 | return backoff
66 | }
67 |
68 | // WithStrategy sets the backoff strategy type (Default, Lineal, Exponential).
69 | //
70 | // If s is not a valid strategy, the function will default to the Default strategy.
71 | func WithStrategy(s strategy) BackoffOptions {
72 | return func(b *Backoff) {
73 | switch s {
74 | case Lineal, Exponential:
75 | b.Strategy = s
76 | b.BackoffDelay = oneSecond
77 | case Default:
78 | b.Strategy = Default
79 | default:
80 | b.Strategy = Default
81 | }
82 | }
83 | }
84 |
85 | // WithBackoffDelay sets an initial backoff delay.
86 | func WithBackoffDelay(delay time.Duration) BackoffOptions {
87 | return func(b *Backoff) {
88 | b.BackoffDelay = delay
89 | }
90 | }
91 |
92 | // WithMaxDelay set the maximum backoff delay.
93 | func WithMaxDelay(delay time.Duration) BackoffOptions {
94 | return func(b *Backoff) {
95 | b.MaxDelay = delay
96 | }
97 | }
98 |
99 | // Next applies the specified backoff strategy to wait before the next retry, adjusting the delay
100 | // based on the strategy (Default, Lineal, or Exponential).
101 | //
102 | // The delay will not exceed maxDelay.
103 | func (b *Backoff) Next() {
104 | switch b.Strategy {
105 | case Default:
106 | time.Sleep(b.BackoffDelay)
107 |
108 | case Lineal:
109 | time.Sleep(b.BackoffDelay)
110 | b.BackoffDelay += oneSecond
111 |
112 | case Exponential:
113 | time.Sleep(b.BackoffDelay)
114 | b.BackoffDelay *= 2
115 | }
116 |
117 | if b.BackoffDelay >= b.MaxDelay {
118 | b.BackoffDelay = b.MaxDelay
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/backoff_test.go:
--------------------------------------------------------------------------------
1 | package ravenTree_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | tree "github.com/AndresXLP/ravenTree"
8 | )
9 |
10 | func TestNewBackoff_WithDefaultValues(t *testing.T) {
11 | maxBackoffDelay := 10 * time.Second
12 | backoff := tree.NewBackoff(
13 | tree.WithStrategy(tree.Default))
14 |
15 | if backoff.BackoffDelay != 0 {
16 | t.Errorf("Expected BackoffDelay to be 0, got %v", backoff.BackoffDelay)
17 | }
18 |
19 | if backoff.MaxDelay != maxBackoffDelay {
20 | t.Errorf("Expected MaxDelay to be %v, got %v", maxBackoffDelay, backoff.MaxDelay)
21 | }
22 |
23 | if backoff.Strategy != tree.Default {
24 | t.Errorf("Expected strategy to be Default, got %v", backoff.Strategy)
25 | }
26 |
27 | since := time.Now()
28 |
29 | backoff.Next()
30 |
31 | elapsed := time.Since(since)
32 |
33 | tolerance := 50 * time.Microsecond
34 | if elapsed > tolerance {
35 | t.Errorf("Expected elapsed to be within %v, got %v", tolerance, elapsed)
36 | }
37 | }
38 |
39 | func TestNewBackoff_WithBackoffOptionsLinealStrategy(t *testing.T) {
40 | customBackoffDelay := 3 * time.Second
41 | customMaxDelay := 5 * time.Second
42 | backoff := tree.NewBackoff(
43 | tree.WithStrategy(tree.Lineal),
44 | tree.WithBackoffDelay(customBackoffDelay),
45 | tree.WithMaxDelay(customMaxDelay),
46 | )
47 |
48 | if backoff.BackoffDelay != customBackoffDelay {
49 | t.Errorf("Expected BackoffDelay to be %v, got %v", customBackoffDelay, backoff.BackoffDelay)
50 | }
51 |
52 | if backoff.MaxDelay != customMaxDelay {
53 | t.Errorf("Expected MaxDelay to be %v, got %v", customMaxDelay, backoff.MaxDelay)
54 | }
55 |
56 | if backoff.Strategy != tree.Lineal {
57 | t.Errorf("Expected Strategy to be %v, got %v", tree.Lineal, backoff.Strategy)
58 | }
59 |
60 | since := time.Now()
61 |
62 | backoff.Next()
63 |
64 | elapsed := time.Since(since)
65 |
66 | tolerance := customBackoffDelay + 50*time.Millisecond
67 | if elapsed > tolerance {
68 | t.Errorf("Expected elapsed to be %v, got %v", tolerance, elapsed)
69 | }
70 | }
71 |
72 | func TestNewBackoff_WithBackoffOptionsExponentialStrategy(t *testing.T) {
73 | backoffDelayExpected := 1 * time.Second
74 | backoff := tree.NewBackoff(
75 | tree.WithStrategy(tree.Exponential),
76 | )
77 |
78 | if backoff.BackoffDelay != backoffDelayExpected {
79 | t.Errorf("Expected BackoffDelay to be %v, got %v", backoffDelayExpected, backoff.BackoffDelay)
80 | }
81 |
82 | if backoff.Strategy != tree.Exponential {
83 | t.Errorf("Expected Strategy to be %v, got %v", tree.Exponential, backoff.Strategy)
84 | }
85 |
86 | since := time.Now()
87 |
88 | backoff.Next()
89 |
90 | elapsed := time.Since(since)
91 |
92 | tolerance := backoffDelayExpected + 50*time.Millisecond
93 | if elapsed > tolerance {
94 | t.Errorf("Expected elapsed to be %v, got %v", tolerance, elapsed)
95 | }
96 | }
97 |
98 | func TestNewBackoff_WithBackoffOptionsMaxDelayLessThanBackoffDelay(t *testing.T) {
99 | overrideDelay := 5 * time.Second
100 | backoff := tree.NewBackoff(
101 | tree.WithStrategy(tree.Lineal),
102 | tree.WithBackoffDelay(overrideDelay),
103 | tree.WithMaxDelay(1*time.Second),
104 | )
105 |
106 | if backoff.MaxDelay != overrideDelay {
107 | t.Errorf("Expected MaxDelay to be %v, got %v", overrideDelay, backoff.MaxDelay)
108 | }
109 |
110 | since := time.Now()
111 |
112 | backoff.Next()
113 |
114 | elapsed := time.Since(since)
115 |
116 | tolerance := overrideDelay + 50*time.Millisecond
117 | if elapsed > tolerance {
118 | t.Errorf("Expected elapsed to be %v, got %v", tolerance, elapsed)
119 | }
120 | }
121 |
122 | func TestNewBackoff_WithBackoffOptionsInvalidStrategy(t *testing.T) {
123 | backoff := tree.NewBackoff(
124 | tree.WithStrategy(5),
125 | )
126 |
127 | if backoff.Strategy != tree.Default {
128 | t.Errorf("Expected Strategy to be %v, got %v", tree.Default, backoff.Strategy)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package ravenTree
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | const (
10 | zero = 0
11 | defaultTimeout = 30 * time.Second
12 | HeaderContentType = "Content-Type"
13 | // MIMEApplicationJSON JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259
14 | MIMEApplicationJSON = "application/json"
15 | )
16 |
17 | // Tree defines the methods that any implementation of a RavenTree must provide.
18 | //
19 | // The Tree interface requires a single method, SendRaven, which sends options
20 | // based on the provided Options and returns a WrapperResponse.
21 | type Tree interface {
22 | // SendRaven sends a raven to a specified URL using the provided Options.
23 | //
24 | // This method constructs an HTTP request based on the given context and Options.
25 | //
26 | // By default, it sets the Content-Type header to application/json.
27 | //
28 | // It sends the request using an HTTP client and returns a
29 | // WrapperResponse that encapsulates the HTTP response.
30 | //
31 | // Parameters:
32 | // - ctx: A context.Context to control the request's lifecycle and manage timeouts.
33 | // - opt: A pointer to an Options struct that contains the necessary configuration
34 | // for the request.
35 | //
36 | // Returns:
37 | // - WrapperResponse: A wrapper around the HTTP response.
38 | // - error: An error if the request fails at any point, or nil if the request is successful.
39 | SendRaven(ctx context.Context, opt *Options) (WrapperResponse, error)
40 | }
41 |
42 | type raven struct {
43 | client *http.Client
44 | }
45 |
46 | // NewRavensTree creates and returns a new instance of the RavenTree interface.
47 | //
48 | // This function acts as a constructor for the RavenTree implementation, returning
49 | // an instance of `raven`, a private struct that implements the `Tree` interface.
50 | // The returned instance includes an internal `http.Client` configured with a default timeout.
51 | //
52 | // Returns:
53 | // - Tree: An object that implements the RavenTree interface with a pre-configured HTTP client.
54 | func NewRavensTree() Tree {
55 | return &raven{
56 | client: &http.Client{
57 | Timeout: defaultTimeout,
58 | },
59 | }
60 | }
61 |
62 | func (r *raven) SendRaven(ctx context.Context, opt *Options) (WrapperResponse, error) {
63 | URL, err := opt.buildURL()
64 | if err != nil {
65 | return WrapperResponse{}, err
66 | }
67 |
68 | body, err := opt.bodyToBufferBody()
69 | if err != nil {
70 | return WrapperResponse{}, err
71 | }
72 |
73 | opt.defaultOptions()
74 |
75 | resp := &http.Response{}
76 | errs := ErrCollections{}
77 |
78 | if opt.Timeout != time.Duration(0) {
79 | r.client.Timeout = opt.Timeout
80 | }
81 |
82 | for i := 0; i < opt.RetryCount; i++ {
83 | req, err := http.NewRequestWithContext(ctx, opt.Method, URL, &body)
84 | if err != nil {
85 | return WrapperResponse{}, err
86 | }
87 |
88 | req.Header.Add(HeaderContentType, MIMEApplicationJSON)
89 |
90 | if len(opt.Headers) > zero {
91 | for key, value := range opt.Headers {
92 | req.Header.Add(key, value)
93 | }
94 | }
95 |
96 | resp, err = r.client.Do(req)
97 | if err != nil {
98 | errs.Add(err.Error())
99 | opt.Backoff.Next()
100 |
101 | continue
102 | }
103 |
104 | if resp.StatusCode >= http.StatusInternalServerError {
105 | opt.Backoff.Next()
106 |
107 | continue
108 | }
109 |
110 | errs.CleanCollection()
111 |
112 | break
113 | }
114 |
115 | return WrapperResponse{resp}, errs.HasError()
116 | }
117 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | package ravenTree_test
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "log"
7 | "net/http"
8 | "net/http/httptest"
9 | "sync"
10 | "testing"
11 | "time"
12 |
13 | "github.com/AndresXLP/ravenTree"
14 | "github.com/stretchr/testify/suite"
15 | )
16 |
17 | type response struct {
18 | Data []string `json:"data"`
19 | }
20 |
21 | var (
22 | queryParams = map[string]string{
23 | "email": "test@test.com",
24 | "username": "tester",
25 | "test": "true",
26 | }
27 |
28 | headers = map[string]string{
29 | "Authorization": "Bearer token",
30 | }
31 |
32 | responsePrettyStringExpected = "{\"data\":[\"test@test.com\",\"tester\",\"true\"]}\n"
33 |
34 | responseQueryParamsExpected = response{Data: []string{
35 | "test@test.com", "tester", "true",
36 | }}
37 |
38 | wrongBody = struct {
39 | Data chan struct{}
40 | }{
41 | Data: make(chan struct{}),
42 | }
43 |
44 | ctx = context.Background()
45 | )
46 |
47 | type ravenTreeTestSuite struct {
48 | suite.Suite
49 | underTest ravenTree.Tree
50 | }
51 |
52 | func TestRavenTreeSuite(t *testing.T) {
53 | suite.Run(t, new(ravenTreeTestSuite))
54 | }
55 | func (suite *ravenTreeTestSuite) SetupTest() {
56 | suite.underTest = ravenTree.NewRavensTree()
57 | }
58 |
59 | func (suite *ravenTreeTestSuite) TestSendRaven_SuccessWithDefaultOptions() {
60 | handler := func(w http.ResponseWriter, r *http.Request) {
61 | // Check request path
62 | suite.Equal("/api/default", r.URL.Path)
63 | // Check request method
64 | suite.Equal(http.MethodGet, r.Method)
65 |
66 | w.WriteHeader(http.StatusOK)
67 | }
68 |
69 | server := httptest.NewServer(http.HandlerFunc(handler))
70 | defer server.Close()
71 |
72 | options := &ravenTree.Options{
73 | Host: server.URL,
74 | Path: "/api/default",
75 | Method: http.MethodGet,
76 | }
77 |
78 | resp, err := suite.underTest.SendRaven(ctx, options)
79 | suite.NoError(err)
80 | suite.Equal(http.StatusOK, resp.StatusCode)
81 | }
82 |
83 | func (suite *ravenTreeTestSuite) TestSendRaven_SuccessWithHeadersAndQueryParams() {
84 | handler := func(w http.ResponseWriter, r *http.Request) {
85 | // Check request path
86 | suite.Equal("/api/query-params", r.URL.Path)
87 | // Check request method
88 | suite.Equal(http.MethodGet, r.Method)
89 | // Check expected header
90 | suite.Equal(headers["Authorization"], r.Header.Get("Authorization"))
91 |
92 | // Extract query parameters
93 | email := r.URL.Query().Get("email")
94 | username := r.URL.Query().Get("username")
95 | test := r.URL.Query().Get("test")
96 |
97 | // Create the response struct
98 | resp := response{
99 | Data: []string{email, username, test},
100 | }
101 |
102 | w.WriteHeader(http.StatusOK)
103 |
104 | // Encode the response into JSON and write it to the response writer
105 | _ = json.NewEncoder(w).Encode(resp)
106 | }
107 |
108 | server := httptest.NewServer(http.HandlerFunc(handler))
109 | defer server.Close()
110 |
111 | options := &ravenTree.Options{
112 | Host: server.URL,
113 | Path: "/api/query-params",
114 | Method: http.MethodGet,
115 | QueryParams: queryParams,
116 | Headers: headers,
117 | }
118 |
119 | resp, err := suite.underTest.SendRaven(ctx, options)
120 |
121 | suite.NoError(err)
122 | suite.Equal(http.StatusOK, resp.StatusCode)
123 |
124 | data := response{}
125 | err = resp.ParseBodyTo(&data)
126 |
127 | suite.NoError(err)
128 | suite.Equal(responseQueryParamsExpected, data)
129 |
130 | stringData := resp.ParseBodyToString()
131 | suite.Equal(responsePrettyStringExpected, stringData)
132 | }
133 |
134 | func (suite *ravenTreeTestSuite) TestSendRaven_SuccessWhenRetryWithoutBackoffStrategy() {
135 | try := 1
136 | mu := &sync.Mutex{} // To prevent race conditions when updating 'try'
137 |
138 | handler := func(w http.ResponseWriter, r *http.Request) {
139 | // Check request path
140 | suite.Equal("/api/retry", r.URL.Path)
141 | // Check request method
142 | suite.Equal(http.MethodGet, r.Method)
143 |
144 | mu.Lock()
145 | if try == 4 {
146 | log.Printf("Successful request on attempt # %d", try)
147 | w.WriteHeader(http.StatusOK)
148 | _, _ = w.Write([]byte(`{}`)) // Empty JSON response
149 |
150 | return
151 | }
152 |
153 | log.Printf("Attempt # %d", try)
154 |
155 | try++
156 | mu.Unlock()
157 |
158 | time.Sleep(3 * time.Second) // Simulate delay for retries
159 | }
160 |
161 | server := httptest.NewServer(http.HandlerFunc(handler))
162 | defer server.Close()
163 |
164 | options := &ravenTree.Options{
165 | Host: server.URL,
166 | Path: "/api/retry",
167 | Method: http.MethodGet,
168 | Timeout: 1 * time.Second,
169 | RetryCount: 5,
170 | }
171 |
172 | since := time.Now()
173 | _, err := suite.underTest.SendRaven(ctx, options)
174 | suite.NoError(err)
175 |
176 | duration := time.Since(since)
177 | expectedDuration := 3 * time.Second
178 | suite.True(duration > expectedDuration && duration < expectedDuration+50*time.Millisecond)
179 | }
180 |
181 | func (suite *ravenTreeTestSuite) TestSendRaven_SuccessWhenRetryWithBackoffLineal() {
182 | try := 1
183 | mu := &sync.Mutex{} // To prevent race conditions when updating 'try'
184 |
185 | handler := func(w http.ResponseWriter, r *http.Request) {
186 | // Check request path
187 | suite.Equal("/api/retry", r.URL.Path)
188 | // Check request method
189 | suite.Equal(http.MethodGet, r.Method)
190 |
191 | mu.Lock()
192 | if try == 4 {
193 | log.Printf("Successful request on attempt # %d", try)
194 | w.WriteHeader(http.StatusOK)
195 | _, _ = w.Write([]byte(`{}`)) // Empty JSON response
196 |
197 | return
198 | }
199 |
200 | log.Printf("Attempt # %d", try)
201 |
202 | try++
203 | mu.Unlock()
204 |
205 | time.Sleep(3 * time.Second) // Simulate delay for retries
206 | }
207 |
208 | server := httptest.NewServer(http.HandlerFunc(handler))
209 | defer server.Close()
210 |
211 | options := &ravenTree.Options{
212 | Host: server.URL,
213 | Path: "/api/retry",
214 | Method: http.MethodGet,
215 | Timeout: 1 * time.Second,
216 | RetryCount: 5,
217 | Backoff: ravenTree.NewBackoff(
218 | ravenTree.WithStrategy(ravenTree.Lineal),
219 | ),
220 | }
221 |
222 | since := time.Now()
223 | _, err := suite.underTest.SendRaven(ctx, options)
224 | suite.NoError(err)
225 |
226 | duration := time.Since(since)
227 | expectedDuration := 9 * time.Second
228 | suite.True(duration > expectedDuration && duration < expectedDuration+50*time.Millisecond)
229 | }
230 |
231 | func (suite *ravenTreeTestSuite) TestSendRaven_SuccessWhenRetryWithBackoffExponential() {
232 | try := 1
233 | mu := &sync.Mutex{} // To prevent race conditions when updating 'try'
234 |
235 | handler := func(w http.ResponseWriter, r *http.Request) {
236 | // Check request path
237 | suite.Equal("/api/retry", r.URL.Path)
238 | // Check request method
239 | suite.Equal(http.MethodGet, r.Method)
240 |
241 | mu.Lock()
242 | if try == 5 {
243 | log.Printf("Successful request on attempt # %d", try)
244 | w.WriteHeader(http.StatusOK)
245 | _, _ = w.Write([]byte(`{}`)) // Empty JSON response
246 |
247 | return
248 | }
249 |
250 | log.Printf("Attempt # %d", try)
251 |
252 | try++
253 | mu.Unlock()
254 |
255 | time.Sleep(3 * time.Second) // Simulate delay for retries
256 | }
257 |
258 | server := httptest.NewServer(http.HandlerFunc(handler))
259 | defer server.Close()
260 |
261 | options := &ravenTree.Options{
262 | Host: server.URL,
263 | Path: "/api/retry",
264 | Method: http.MethodGet,
265 | Timeout: 1 * time.Second,
266 | RetryCount: 5,
267 | Backoff: ravenTree.NewBackoff(
268 | ravenTree.WithStrategy(ravenTree.Exponential),
269 | ravenTree.WithMaxDelay(15*time.Second),
270 | ),
271 | }
272 |
273 | since := time.Now()
274 | _, err := suite.underTest.SendRaven(ctx, options)
275 | suite.NoError(err)
276 |
277 | duration := time.Since(since)
278 | expectedDuration := 19 * time.Second
279 | suite.True(duration > expectedDuration && duration < expectedDuration+50*time.Millisecond)
280 | }
281 |
282 | func (suite *ravenTreeTestSuite) TestSendRaven_FailWhenTimedOut() {
283 | handler := func(w http.ResponseWriter, r *http.Request) {
284 | // Check request path
285 | suite.Equal("/api/timeout", r.URL.Path)
286 | // Check request method
287 | suite.Equal(http.MethodGet, r.Method)
288 |
289 | time.Sleep(2 * time.Second)
290 | }
291 |
292 | server := httptest.NewServer(http.HandlerFunc(handler))
293 | defer server.Close()
294 |
295 | options := &ravenTree.Options{
296 | Host: server.URL,
297 | Path: "/api/timeout",
298 | Method: http.MethodGet,
299 | Timeout: 1 * time.Second,
300 | }
301 |
302 | _, err := suite.underTest.SendRaven(ctx, options)
303 |
304 | suite.Error(err)
305 | suite.ErrorContains(err, "context deadline exceeded (Client.Timeout exceeded while awaiting headers)")
306 | }
307 |
308 | func (suite *ravenTreeTestSuite) TestSendRaven_FailWhenInvalidURL() {
309 | options := &ravenTree.Options{
310 | Host: ":foo",
311 | }
312 |
313 | _, err := suite.underTest.SendRaven(ctx, options)
314 | suite.Error(err)
315 | }
316 |
317 | func (suite *ravenTreeTestSuite) TestSendRaven_FailWhenInvalidPath() {
318 | options := &ravenTree.Options{
319 | Host: "http://localhost:8080",
320 | Path: ":foo",
321 | }
322 |
323 | _, err := suite.underTest.SendRaven(ctx, options)
324 | suite.Error(err)
325 | }
326 |
327 | func (suite *ravenTreeTestSuite) TestSendRaven_FailRequestInvalidMethod() {
328 | options := &ravenTree.Options{
329 | Host: "http://localhost:8080",
330 | Path: "/api",
331 | Method: "😰",
332 | }
333 |
334 | _, err := suite.underTest.SendRaven(ctx, options)
335 | suite.Error(err)
336 | }
337 |
338 | func (suite *ravenTreeTestSuite) TestSendRaven_FailWhenInvalidBody() {
339 | options := &ravenTree.Options{
340 | Host: "http://localhost:8080",
341 | Path: "/api/retry",
342 | Method: http.MethodGet,
343 | Body: wrongBody,
344 | }
345 |
346 | _, err := suite.underTest.SendRaven(ctx, options)
347 | suite.Error(err)
348 | }
349 |
350 | func (suite *ravenTreeTestSuite) TestSendRaven_FailWhenInternalServerError() {
351 | handler := func(w http.ResponseWriter, r *http.Request) {
352 | w.WriteHeader(http.StatusInternalServerError)
353 | }
354 |
355 | server := httptest.NewServer(http.HandlerFunc(handler))
356 | defer server.Close()
357 |
358 | options := &ravenTree.Options{
359 | Host: server.URL,
360 | Path: "/api/error",
361 | Method: http.MethodGet,
362 | }
363 |
364 | resp, err := suite.underTest.SendRaven(ctx, options)
365 | suite.NoError(err)
366 | suite.Equal(http.StatusInternalServerError, resp.StatusCode)
367 | }
368 |
--------------------------------------------------------------------------------
/docs/ravenTree-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AndresXLP/ravenTree/34ed28adf20771ff8ac84b2f5b12154b73f59f42/docs/ravenTree-logo.png
--------------------------------------------------------------------------------
/err_collections.go:
--------------------------------------------------------------------------------
1 | package ravenTree
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strings"
7 | "sync"
8 | )
9 |
10 | type ErrCollections struct {
11 | mutex sync.Mutex
12 | errs []error
13 | }
14 |
15 | func (ec *ErrCollections) Add(errString string) {
16 | ec.mutex.Lock()
17 | defer ec.mutex.Unlock()
18 | ec.errs = append(ec.errs, errors.New(errString))
19 | }
20 |
21 | func (ec *ErrCollections) Error() string {
22 | errsMessages := make([]string, len(ec.errs))
23 | ec.mutex.Lock()
24 | defer ec.mutex.Unlock()
25 |
26 | for i, e := range ec.errs {
27 | errsMessages[i] = fmt.Sprintf("%s\n", e.Error())
28 | }
29 |
30 | return strings.Join(errsMessages, "")
31 | }
32 |
33 | func (ec *ErrCollections) HasError() error {
34 | ec.mutex.Lock()
35 | defer ec.mutex.Unlock()
36 |
37 | if len(ec.errs) == 0 {
38 | return nil
39 | }
40 |
41 | return ec
42 | }
43 |
44 | func (ec *ErrCollections) CleanCollection() {
45 | ec.mutex.Lock()
46 | defer ec.mutex.Unlock()
47 | ec.errs = []error{}
48 | }
49 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/AndresXLP/ravenTree
2 |
3 | go 1.22
4 |
5 | require github.com/stretchr/testify v1.9.0
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | gopkg.in/yaml.v3 v3.0.1 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 |
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package ravenTree
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net/url"
7 | "time"
8 | )
9 |
10 | const (
11 | defaultRetryCount = 1
12 | )
13 |
14 | type Options struct {
15 | Host string
16 | Path string
17 | Method string
18 | Body interface{}
19 | QueryParams map[string]string
20 | Headers map[string]string
21 | Timeout time.Duration
22 | RetryCount int
23 | Backoff *Backoff
24 | }
25 |
26 | // bodyToBufferBody serializes the Body field into JSON and stores it in a bytes.Buffer.
27 | //
28 | // It marshals the `Body` field of the `Options` struct into a JSON byte slice. If an error occurs
29 | // during the marshalling process, it returns an empty buffer and the error.
30 | //
31 | // Returns:
32 | // - bytes.Buffer: A buffer containing the serialized JSON of the `Body` field.
33 | // - error: An error that occurs during the JSON marshalling process, if any.
34 | func (o *Options) bodyToBufferBody() (bytes.Buffer, error) {
35 | bytesJson, err := json.Marshal(o.Body)
36 | if err != nil {
37 | return bytes.Buffer{}, err
38 | }
39 |
40 | return *bytes.NewBuffer(bytesJson), nil
41 | }
42 |
43 | // buildURL constructs a full URL by combining the Host, Path, and QueryParams fields from the Options struct.
44 | //
45 | // It first parses the `Host` and `Path` fields into URLs. Then, it resolves the path relative to the base URL.
46 | // If `QueryParams` are provided, they are appended to the final URL as query parameters.
47 | //
48 | // Returns:
49 | // - string: The fully constructed URL as a string.
50 | // - error: An error if there is a problem parsing the Host or Path.
51 | func (o *Options) buildURL() (string, error) {
52 | baseURL, err := url.Parse(o.Host)
53 | if err != nil {
54 | return "", err
55 | }
56 |
57 | pathUrl, err := url.Parse(o.Path)
58 | if err != nil {
59 | return "", err
60 | }
61 |
62 | finalURL := baseURL.ResolveReference(pathUrl)
63 |
64 | if len(o.QueryParams) > 0 {
65 | query := finalURL.Query()
66 | for key, value := range o.QueryParams {
67 | query.Add(key, value)
68 | }
69 |
70 | finalURL.RawQuery = query.Encode()
71 | }
72 |
73 | return finalURL.String(), nil
74 | }
75 |
76 | // defaultOptions sets default values for any unset fields in the Options struct.
77 | // If certain fields are not initialized by the user, this method assigns sensible defaults:
78 | // - Backoff: Default to the standard backoff.
79 | // - RetryCount: Defaults to 1.
80 | func (o *Options) defaultOptions() {
81 | if o.Backoff == nil {
82 | o.Backoff = NewBackoff()
83 | }
84 |
85 | if o.RetryCount == zero {
86 | o.RetryCount = defaultRetryCount
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/wrapper.go:
--------------------------------------------------------------------------------
1 | package ravenTree
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | type WrapperResponse struct {
11 | *http.Response
12 | }
13 |
14 | // ParseBodyTo parses the HTTP response body into the provided destination (dest).
15 | //
16 | // This method reads the response body (w.Body) as a byte slice, then attempts
17 | // to unmarshal the JSON content into the provided `dest` interface.
18 | //
19 | // The response body (w.Body) is then restored so it can be read again later, if necessary.
20 | //
21 | // Parameters:
22 | // - dest (interface{}): A pointer to the destination where the parsed JSON
23 | // from the response body will be stored. It can be a struct or map that matches
24 | // the JSON structure.
25 | //
26 | // Returns:
27 | // - error: Returns an error if the reading of the body or the unmarshalling process fails.
28 | func (w *WrapperResponse) ParseBodyTo(dest interface{}) error {
29 | bodyBytes, _ := io.ReadAll(w.Body)
30 | w.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
31 |
32 | return json.Unmarshal(bodyBytes, dest)
33 | }
34 |
35 | // ParseBodyToString reads the HTTP response body and returns it as a string.
36 | //
37 | // This method reads the entire response body (w.Body) into a byte slice, converts it
38 | // to a string, and then restores the body so it can be read again later if needed.
39 | //
40 | // Returns:
41 | // - string: The response body as a string.
42 | func (w *WrapperResponse) ParseBodyToString() string {
43 | bodyBytes, _ := io.ReadAll(w.Body)
44 | w.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
45 |
46 | return string(bodyBytes)
47 | }
48 |
--------------------------------------------------------------------------------