funnel_http 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cc6d1f5ffef3ad3e41de07eb3b94342bd082724aa3d1daad338603fbedbabcdc
4
+ data.tar.gz: f3208ba0c9154bd99fd2d373e91d3679690f0f711c74a3f11038c500c08fbd8a
5
+ SHA512:
6
+ metadata.gz: 87f9ad18e8c0005f2a3520d06381f3e308bbf6372827b631d646546c48f929be5879da1296331d9bab4ec7667786ce353473930b6795b84d0a46c11f9e68ec12
7
+ data.tar.gz: c92e03744f09aa670972e188bb0c94db2b2920f028994db5bb0f26d3804d470345b43087d57195666d1236bfc2adcc9c8ee8ccde4c0ca50ae94b54887a0c1dd6
data/.golangci.yml ADDED
@@ -0,0 +1,21 @@
1
+ linters-settings:
2
+ gofmt:
3
+ revive:
4
+ rules:
5
+ - name: exported
6
+ arguments:
7
+ - disableStutteringCheck
8
+ testifylint:
9
+ wrapcheck:
10
+
11
+ linters:
12
+ enable:
13
+ - gofmt
14
+ - revive
15
+ - testifylint
16
+ - wrapcheck
17
+
18
+ issues:
19
+ include:
20
+ - EXC0012 # EXC0012 revive: Annoying issue about not having a comment. The rare codebase has such comments
21
+ - EXC0014 # EXC0014 revive: Annoying issue about not having a comment. The rare codebase has such comments
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format progress
2
+ --color
3
+ --require spec_helper
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --markup markdown
2
+ --no-private
3
+ --hide-void-return
4
+ -
5
+ CHANGELOG.md
6
+ LICENSE.txt
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ ## [Unreleased]
2
+ [full changelog](http://github.com/sue445/funnel_http/compare/v0.1.0...main)
3
+
4
+ ## [0.1.0](https://github.com/sue445/funnel_http/releases/tag/v0.1.0) - 2024-12-18
5
+
6
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 sue445
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # FunnelHttp
2
+ Perform HTTP requests in parallel
3
+
4
+ [![build](https://github.com/sue445/funnel_http/actions/workflows/build.yml/badge.svg)](https://github.com/sue445/funnel_http/actions/workflows/build.yml)
5
+
6
+ ## Requirements
7
+ * Ruby
8
+ * Go
9
+
10
+ ## Installation
11
+
12
+ Install the gem and add to the application's Gemfile by executing:
13
+
14
+ ```bash
15
+ bundle add funnel_http
16
+ ```
17
+
18
+ If bundler is not being used to manage dependencies, install the gem by executing:
19
+
20
+ ```bash
21
+ gem install funnel_http
22
+ ```
23
+
24
+ ## Usage
25
+ Use [`FunnelHttp::Client#perform`](https://sue445.github.io/funnel_http/FunnelHttp/Client.html#perform-instance_method)
26
+
27
+ ```ruby
28
+ require "funnel_http"
29
+
30
+ client = FunnelHttp::Client.new
31
+
32
+ requests = [
33
+ {
34
+ method: :get,
35
+ uri: "https://example.com/api/user/1",
36
+ },
37
+
38
+ # with request header
39
+ {
40
+ method: :get,
41
+ uri: "https://example.com/api/user/2",
42
+ header: {
43
+ "Authorization" => "Bearer xxxxxxxx",
44
+ "X-Multiple-Values" => ["1st value", "2nd value"],
45
+ },
46
+ },
47
+ ]
48
+
49
+ responses = client.perform(requests)
50
+ # => [
51
+ # { status_code: 200, body: "Response of /api/user/1", header: { "Content-Type" => ["text/plain;charset=utf-8"]} }
52
+ # { status_code: 200, body: "Response of /api/user/2", header: { "Content-Type" => ["text/plain;charset=utf-8"]} }
53
+ # ]
54
+ ```
55
+
56
+ ## Customize
57
+ ### Add default header
58
+ Example1. Pass default header to [`FunnelHttp::Client#initialize`](https://sue445.github.io/funnel_http/FunnelHttp/Client.html#normalize_requests-instance_method)
59
+
60
+ ```ruby
61
+ default_header = { "Authorization" => "Bearer xxxxxx" }
62
+
63
+ client = FunnelHttp::Client.new(default_header)
64
+ ```
65
+
66
+ Example 2. Use [`FunnelHttp::Client#add_default_request_header`](https://sue445.github.io/funnel_http/FunnelHttp/Client.html#add_default_request_header-instance_method)
67
+
68
+ ```ruby
69
+ client.add_default_request_header("Authorization", "Bearer xxxxxx")
70
+ ```
71
+
72
+ ## API Reference
73
+ https://sue445.github.io/funnel_http/
74
+
75
+ ## Development
76
+
77
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
78
+
79
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
80
+
81
+ ## Contributing
82
+
83
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sue445/funnel_http.
84
+
85
+ ## License
86
+
87
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rake/extensiontask"
9
+
10
+ task build: :compile
11
+
12
+ GEMSPEC = Gem::Specification.load("funnel_http.gemspec")
13
+
14
+ Rake::ExtensionTask.new("funnel_http", GEMSPEC) do |ext|
15
+ ext.lib_dir = "lib/funnel_http"
16
+ end
17
+
18
+ require "go_gem/rake_task"
19
+
20
+ go_task = GoGem::RakeTask.new("funnel_http")
21
+
22
+ namespace :go do
23
+ desc "Run golangci-lint"
24
+ task :lint do
25
+ go_task.within_target_dir do
26
+ sh "which golangci-lint" do |ok, _|
27
+ raise "golangci-lint isn't installed. See. https://golangci-lint.run/welcome/install/" unless ok
28
+ end
29
+ sh GoGem::RakeTask.build_env_vars, "golangci-lint run"
30
+ end
31
+ end
32
+
33
+ desc "Run go mod tidy"
34
+ task :mod_tidy do
35
+ go_task.within_target_dir do
36
+ sh "go mod tidy"
37
+ end
38
+ end
39
+ end
40
+
41
+ namespace :rbs do
42
+ desc "`rbs collection install` and `git commit`"
43
+ task :install do
44
+ sh "rbs collection install"
45
+ sh "git add rbs_collection.lock.yaml"
46
+ sh "git commit -m 'rbs collection install' || true"
47
+ end
48
+ end
49
+
50
+ desc "Check rbs"
51
+ task :rbs do
52
+ sh "rbs validate"
53
+ sh "steep check"
54
+ end
55
+
56
+ task default: %i[clobber compile go:test spec]
data/Steepfile ADDED
@@ -0,0 +1,31 @@
1
+ # D = Steep::Diagnostic
2
+
3
+ target :lib do
4
+ signature "sig"
5
+
6
+ check "lib" # Directory name
7
+ # check "Gemfile" # File name
8
+ # check "app/models/**/*.rb" # Glob
9
+ # ignore "lib/templates/*.rb"
10
+
11
+ # library "pathname" # Standard libraries
12
+ # library "strong_json" # Gems
13
+
14
+ collection_config "rbs_collection.yaml"
15
+
16
+ # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
17
+ # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
18
+ # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
19
+ # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
20
+ # configure_code_diagnostics do |hash| # You can setup everything yourself
21
+ # hash[D::Ruby::NoMethod] = :information
22
+ # end
23
+ end
24
+
25
+ # target :test do
26
+ # signature "sig", "sig-private"
27
+ #
28
+ # check "test"
29
+ #
30
+ # # library "pathname" # Standard libraries
31
+ # end
@@ -0,0 +1,32 @@
1
+ # benchmark for funnel_http
2
+ ## Usage
3
+ ```bash
4
+ docker compose up --build
5
+ ```
6
+
7
+ ```bash
8
+ bundle exec ruby benchmark.rb
9
+ ```
10
+
11
+ ## Report
12
+ ```
13
+ Warming up --------------------------------------
14
+ FunnelHttp::Client#perform
15
+ 2.000 i/100ms
16
+ Parallel with 4 processes
17
+ 1.000 i/100ms
18
+ Parallel with 4 threads
19
+ 1.000 i/100ms
20
+ Calculating -------------------------------------
21
+ FunnelHttp::Client#perform
22
+ 21.816 (± 4.6%) i/s (45.84 ms/i) - 44.000 in 2.026960s
23
+ Parallel with 4 processes
24
+ 15.785 (± 6.3%) i/s (63.35 ms/i) - 32.000 in 2.035628s
25
+ Parallel with 4 threads
26
+ 18.570 (±10.8%) i/s (53.85 ms/i) - 37.000 in 2.008485s
27
+
28
+ Comparison:
29
+ FunnelHttp::Client#perform: 21.8 i/s
30
+ Parallel with 4 threads: 18.6 i/s - 1.17x slower
31
+ Parallel with 4 processes: 15.8 i/s - 1.38x slower
32
+ ```
@@ -0,0 +1,61 @@
1
+ require "benchmark/ips"
2
+ require "open-uri"
3
+ require "parallel"
4
+ require "etc"
5
+
6
+ ROOT_DIR = File.expand_path("..", __dir__)
7
+
8
+ TEST_SERVER_URL = "http://localhost:8080/"
9
+
10
+ REQUEST_COUNT = 100
11
+
12
+ BENCHMARK_CONCURRENCY = ENV.fetch("BENCHMARK_CONCURRENCY") { 4 }
13
+
14
+ # Build native extension before running benchmark
15
+ Dir.chdir(ROOT_DIR) do
16
+ system("bundle config set --local path 'vendor/bundle'", exception: true)
17
+ system("bundle install", exception: true)
18
+ system("bundle exec rake clobber compile", exception: true)
19
+ end
20
+
21
+ require_relative "../lib/funnel_http"
22
+
23
+ # Suppress Ractor warning
24
+ $VERBOSE = nil
25
+
26
+ system("go version", exception: true)
27
+
28
+ requests = Array.new(REQUEST_COUNT, { method: :get, url: TEST_SERVER_URL })
29
+
30
+ def fetch_server
31
+ URI.parse(TEST_SERVER_URL).open(open_timeout: 90, read_timeout: 90).read
32
+ end
33
+
34
+ Benchmark.ips do |x|
35
+ x.config(warmup: 1, time: 2)
36
+
37
+ x.report("FunnelHttp::Client#perform") do
38
+ FunnelHttp::Client.new.perform(requests)
39
+ end
40
+
41
+ x.report("Parallel with #{BENCHMARK_CONCURRENCY} processes") do
42
+ Parallel.each(requests, in_processes: BENCHMARK_CONCURRENCY) do
43
+ fetch_server
44
+ end
45
+ end
46
+
47
+ x.report("Parallel with #{BENCHMARK_CONCURRENCY} threads") do
48
+ Parallel.each(requests, in_threads: BENCHMARK_CONCURRENCY) do
49
+ fetch_server
50
+ end
51
+ end
52
+
53
+ # FIXME: open-uri doesn't work in Ractor
54
+ # x.report("Parallel with Ractor") do
55
+ # REQUEST_COUNT.times.map do
56
+ # Ractor.new { URI.parse("http://localhost:8080/").read }
57
+ # end.each(&:take)
58
+ # end
59
+
60
+ x.compare!
61
+ end
@@ -0,0 +1,8 @@
1
+ services:
2
+ nginx:
3
+ image: nginx:latest
4
+ ports:
5
+ - "8080:80"
6
+ volumes:
7
+ - ./html:/usr/share/nginx/html
8
+ - ./nginx.conf:/etc/nginx/nginx.conf
@@ -0,0 +1,4 @@
1
+ <html>
2
+ <head></head>
3
+ <body>it works</body>
4
+ </html>
@@ -0,0 +1,25 @@
1
+ worker_processes auto;
2
+ worker_rlimit_nofile 12288;
3
+
4
+ events {
5
+ worker_connections 4096;
6
+ }
7
+
8
+ http {
9
+ include mime.types;
10
+ default_type application/octet-stream;
11
+
12
+ sendfile on;
13
+ keepalive_timeout 65;
14
+ access_log off;
15
+
16
+ server {
17
+ listen 80;
18
+ server_name localhost;
19
+
20
+ location / {
21
+ root /usr/share/nginx/html;
22
+ index index.html;
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "go_gem/mkmf"
5
+
6
+ # Makes all symbols private by default to avoid unintended conflict
7
+ # with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
8
+ # selectively, or entirely remove this flag.
9
+ append_cflags("-fvisibility=hidden")
10
+
11
+ create_go_makefile("funnel_http/funnel_http")
@@ -0,0 +1,2 @@
1
+ #include "funnel_http.h"
2
+ #include "_cgo_export.h"
@@ -0,0 +1,164 @@
1
+ package main
2
+
3
+ /*
4
+ #include "funnel_http.h"
5
+
6
+ VALUE rb_go_data_alloc(VALUE klass);
7
+ VALUE rb_funnel_http_run_requests(VALUE self, VALUE rbAry);
8
+ */
9
+ import "C"
10
+
11
+ import (
12
+ "github.com/ruby-go-gem/go-gem-wrapper/ruby"
13
+ "net/http"
14
+ "unsafe"
15
+ )
16
+
17
+ //export rb_funnel_http_run_requests
18
+ func rb_funnel_http_run_requests(self C.VALUE, rbAry C.VALUE) C.VALUE {
19
+ rbAryLength := int(ruby.RARRAY_LEN(ruby.VALUE(rbAry)))
20
+ requests := make([]Request, 0, rbAryLength)
21
+
22
+ for i := 0; i < rbAryLength; i++ {
23
+ rbHash := ruby.RbAryEntry(ruby.VALUE(rbAry), ruby.Long(i))
24
+
25
+ req := Request{
26
+ Method: getRbHashValueAsString(rbHash, "method"),
27
+ URL: getRbHashValueAsString(rbHash, "url"),
28
+ Header: getRbHashValueAsMap(rbHash, "header"),
29
+ }
30
+ requests = append(requests, req)
31
+ }
32
+
33
+ httpClient := getHttpClientFromInstanceVariable(ruby.VALUE(self))
34
+
35
+ responses, err := RunRequests(&httpClient, requests)
36
+ if err != nil {
37
+ ruby.RbRaise(rb_cFunnelHttpError, "%s", err.Error())
38
+ }
39
+
40
+ var rbHashSlice []ruby.VALUE
41
+ for _, response := range responses {
42
+ rbHash := ruby.RbHashNew()
43
+
44
+ ruby.RbHashAset(rbHash, ruby.RbId2Sym(ruby.RbIntern("status_code")), ruby.INT2NUM(response.StatusCode))
45
+ ruby.RbHashAset(rbHash, ruby.RbId2Sym(ruby.RbIntern("body")), ruby.String2Value(string(response.Body)))
46
+
47
+ rbHashHeader := ruby.RbHashNew()
48
+ ruby.RbGcRegisterAddress(&rbHashHeader)
49
+ defer ruby.RbGcUnregisterAddress(&rbHashHeader)
50
+
51
+ for key, values := range response.Header {
52
+ var headerValues []ruby.VALUE
53
+ for _, value := range values {
54
+ v := ruby.String2Value(value)
55
+ ruby.RbGcRegisterAddress(&v)
56
+ defer ruby.RbGcUnregisterAddress(&v)
57
+
58
+ headerValues = append(headerValues, v)
59
+ }
60
+ k := ruby.String2Value(key)
61
+ ruby.RbGcRegisterAddress(&k)
62
+ defer ruby.RbGcUnregisterAddress(&k)
63
+
64
+ v := ruby.Slice2rbAry(headerValues)
65
+ ruby.RbGcRegisterAddress(&v)
66
+ defer ruby.RbGcUnregisterAddress(&v)
67
+
68
+ ruby.RbHashAset(rbHashHeader, k, v)
69
+ }
70
+ ruby.RbHashAset(rbHash, ruby.RbId2Sym(ruby.RbIntern("header")), rbHashHeader)
71
+
72
+ rbHashSlice = append(rbHashSlice, rbHash)
73
+ }
74
+
75
+ return C.VALUE(ruby.Slice2rbAry(rbHashSlice))
76
+ }
77
+
78
+ func getHttpClientFromInstanceVariable(self ruby.VALUE) http.Client {
79
+ id := ruby.RbIntern("@__go_data")
80
+ value := ruby.RbIvarGet(self, id)
81
+
82
+ if !ruby.RB_NIL_P(value) {
83
+ // return http.Client in instance variable
84
+ data := (*goData)(ruby.GetGoStruct(value))
85
+ return data.httpClient
86
+ }
87
+
88
+ // Create instance of FunnelHttp::Ext::GoData
89
+ obj := ruby.RbObjAlloc(rb_cFunnelHttpExtGoData)
90
+ data := ruby.GetGoStruct(obj)
91
+
92
+ // Save FunnelHttp::Ext::GoData to instance variable of FunnelHttp::Ext::Client
93
+ dataValue := (*ruby.VALUE)(data)
94
+ ruby.RbIvarSet(self, id, *dataValue)
95
+
96
+ return ((*goData)(data)).httpClient
97
+ }
98
+
99
+ type goData struct {
100
+ httpClient http.Client
101
+ }
102
+
103
+ //export rb_go_data_alloc
104
+ func rb_go_data_alloc(klass C.VALUE) C.VALUE {
105
+ data := goData{}
106
+ return C.VALUE(ruby.NewGoStruct(ruby.VALUE(klass), unsafe.Pointer(&data)))
107
+ }
108
+
109
+ func getRbHashValueAsString(rbHash ruby.VALUE, key string) string {
110
+ value := ruby.RbHashAref(rbHash, ruby.RbToSymbol(ruby.String2Value(key)))
111
+ return ruby.Value2String(value)
112
+ }
113
+
114
+ func getRbHashValueAsMap(rbHash ruby.VALUE, key string) map[string][]string {
115
+ rbHashValue := ruby.RbHashAref(rbHash, ruby.RbToSymbol(ruby.String2Value(key)))
116
+ rbKeys := ruby.CallFunction(rbHashValue, "keys")
117
+
118
+ var ret = map[string][]string{}
119
+
120
+ for i := 0; i < int(ruby.RARRAY_LEN(rbKeys)); i++ {
121
+ rbKey := ruby.RbAryEntry(rbKeys, ruby.Long(i))
122
+ rbAryValue := ruby.RbHashAref(rbHashValue, rbKey)
123
+
124
+ var values []string
125
+ for j := 0; j < int(ruby.RARRAY_LEN(rbAryValue)); j++ {
126
+ str := ruby.Value2String(ruby.RbAryEntry(rbAryValue, ruby.Long(j)))
127
+ values = append(values, str)
128
+ }
129
+
130
+ keyName := ruby.Value2String(rbKey)
131
+ ret[keyName] = values
132
+ }
133
+
134
+ return ret
135
+ }
136
+
137
+ // revive:disable:exported
138
+
139
+ var rb_cFunnelHttpError ruby.VALUE
140
+ var rb_cFunnelHttpExtGoData ruby.VALUE
141
+
142
+ //export Init_funnel_http
143
+ func Init_funnel_http() {
144
+ rb_mFunnelHttp := ruby.RbDefineModule("FunnelHttp")
145
+
146
+ // FunnelHttp::Ext
147
+ rb_mFunnelHttpExt := ruby.RbDefineModuleUnder(rb_mFunnelHttp, "Ext")
148
+
149
+ // FunnelHttp::Ext::Client
150
+ rb_cFunnelHttpExtClient := ruby.RbDefineClassUnder(rb_mFunnelHttpExt, "Client", ruby.VALUE(C.rb_cObject))
151
+ ruby.RbDefineMethod(rb_cFunnelHttpExtClient, "run_requests", C.rb_funnel_http_run_requests, 1)
152
+
153
+ // FunnelHttp::Ext::GoData
154
+ rb_cFunnelHttpExtGoData = ruby.RbDefineClassUnder(rb_mFunnelHttpExt, "GoData", ruby.VALUE(C.rb_cObject))
155
+ ruby.RbDefineAllocFunc(rb_cFunnelHttpExtGoData, C.rb_go_data_alloc)
156
+
157
+ // FunnelHttp::Error
158
+ rb_cFunnelHttpError = ruby.RbDefineClassUnder(rb_mFunnelHttp, "Error", ruby.VALUE(C.rb_eStandardError))
159
+ }
160
+
161
+ // revive:enable:exported
162
+
163
+ func main() {
164
+ }
@@ -0,0 +1,6 @@
1
+ #ifndef FUNNEL_HTTP_H
2
+ #define FUNNEL_HTTP_H 1
3
+
4
+ #include "ruby.h"
5
+
6
+ #endif /* FUNNEL_HTTP_H */
@@ -0,0 +1,27 @@
1
+ module github.com/sue445/funnel_http
2
+
3
+ go 1.23
4
+
5
+ require (
6
+ github.com/cockroachdb/errors v1.11.3
7
+ github.com/jarcoal/httpmock v1.3.1
8
+ github.com/ruby-go-gem/go-gem-wrapper v0.5.1
9
+ github.com/stretchr/testify v1.10.0
10
+ golang.org/x/sync v0.10.0
11
+ )
12
+
13
+ require (
14
+ github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
15
+ github.com/cockroachdb/redact v1.1.5 // indirect
16
+ github.com/davecgh/go-spew v1.1.1 // indirect
17
+ github.com/getsentry/sentry-go v0.27.0 // indirect
18
+ github.com/gogo/protobuf v1.3.2 // indirect
19
+ github.com/kr/pretty v0.3.1 // indirect
20
+ github.com/kr/text v0.2.0 // indirect
21
+ github.com/pkg/errors v0.9.1 // indirect
22
+ github.com/pmezard/go-difflib v1.0.0 // indirect
23
+ github.com/rogpeppe/go-internal v1.9.0 // indirect
24
+ golang.org/x/sys v0.18.0 // indirect
25
+ golang.org/x/text v0.14.0 // indirect
26
+ gopkg.in/yaml.v3 v3.0.1 // indirect
27
+ )