funnel_http 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.golangci.yml +21 -0
- data/.rspec +3 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +87 -0
- data/Rakefile +56 -0
- data/Steepfile +31 -0
- data/benchmark/README.md +32 -0
- data/benchmark/benchmark.rb +61 -0
- data/benchmark/compose.yml +8 -0
- data/benchmark/html/index.html +4 -0
- data/benchmark/nginx.conf +25 -0
- data/ext/funnel_http/extconf.rb +11 -0
- data/ext/funnel_http/funnel_http.c +2 -0
- data/ext/funnel_http/funnel_http.go +164 -0
- data/ext/funnel_http/funnel_http.h +6 -0
- data/ext/funnel_http/go.mod +27 -0
- data/ext/funnel_http/go.sum +78 -0
- data/ext/funnel_http/run_requests.go +77 -0
- data/ext/funnel_http/run_requests_test.go +120 -0
- data/lib/funnel_http/client.rb +112 -0
- data/lib/funnel_http/ext.rb +6 -0
- data/lib/funnel_http/version.rb +5 -0
- data/lib/funnel_http.rb +14 -0
- data/rbs_collection.lock.yaml +68 -0
- data/rbs_collection.yaml +24 -0
- data/sig/funnel_http/client.rbs +21 -0
- data/sig/funnel_http/ext.rbs +11 -0
- data/sig/funnel_http.rbs +31 -0
- metadata +261 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
2
|
+
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
3
|
+
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
4
|
+
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
5
|
+
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
6
|
+
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
7
|
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
8
|
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
9
|
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
10
|
+
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
11
|
+
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
12
|
+
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
13
|
+
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
14
|
+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
15
|
+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
16
|
+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
17
|
+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
18
|
+
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
19
|
+
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
20
|
+
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
21
|
+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
22
|
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
23
|
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
24
|
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
25
|
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
26
|
+
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
27
|
+
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
28
|
+
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
29
|
+
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
30
|
+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
31
|
+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
32
|
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
33
|
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
34
|
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
35
|
+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
36
|
+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
37
|
+
github.com/ruby-go-gem/go-gem-wrapper v0.5.1 h1:TFGH/eOJl0uYzYMwrcLbZxXNQiQbQjCq/VLXyRk1HRM=
|
38
|
+
github.com/ruby-go-gem/go-gem-wrapper v0.5.1/go.mod h1:k2k+LziSCMxNYP4J9/9v90xdU6zlU1DJpJDTU6oJhHE=
|
39
|
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
40
|
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
41
|
+
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
42
|
+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
43
|
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
44
|
+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
45
|
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
46
|
+
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
47
|
+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
48
|
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
49
|
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
50
|
+
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
51
|
+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
52
|
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
53
|
+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
54
|
+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
55
|
+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
56
|
+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
57
|
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
58
|
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
59
|
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
60
|
+
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
61
|
+
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
62
|
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
63
|
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
64
|
+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
65
|
+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
66
|
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
67
|
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
68
|
+
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
69
|
+
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
70
|
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
71
|
+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
72
|
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
73
|
+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
74
|
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
75
|
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
76
|
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
77
|
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
78
|
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@@ -0,0 +1,77 @@
|
|
1
|
+
package main
|
2
|
+
|
3
|
+
import (
|
4
|
+
"bytes"
|
5
|
+
"github.com/cockroachdb/errors"
|
6
|
+
"golang.org/x/sync/errgroup"
|
7
|
+
"io"
|
8
|
+
"net/http"
|
9
|
+
)
|
10
|
+
|
11
|
+
// Request is proxy between CRuby and Go
|
12
|
+
type Request struct {
|
13
|
+
Method string
|
14
|
+
URL string
|
15
|
+
Header map[string][]string
|
16
|
+
}
|
17
|
+
|
18
|
+
// Response is proxy between CRuby and Go
|
19
|
+
type Response struct {
|
20
|
+
StatusCode int
|
21
|
+
Header map[string][]string
|
22
|
+
Body []byte
|
23
|
+
}
|
24
|
+
|
25
|
+
// RunRequests perform HTTP requests in parallel
|
26
|
+
func RunRequests(httpClient *http.Client, requests []Request) ([]Response, error) {
|
27
|
+
g := new(errgroup.Group)
|
28
|
+
responses := make([]Response, len(requests))
|
29
|
+
|
30
|
+
for i, request := range requests {
|
31
|
+
// https://golang.org/doc/faq#closures_and_goroutines
|
32
|
+
i := i
|
33
|
+
request := request
|
34
|
+
|
35
|
+
g.Go(func() error {
|
36
|
+
var body []byte
|
37
|
+
httpReq, err := http.NewRequest(request.Method, request.URL, bytes.NewBuffer(body))
|
38
|
+
if err != nil {
|
39
|
+
return errors.WithStack(err)
|
40
|
+
}
|
41
|
+
|
42
|
+
for key, values := range request.Header {
|
43
|
+
for _, value := range values {
|
44
|
+
httpReq.Header.Add(key, value)
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
httpResp, err := httpClient.Do(httpReq)
|
49
|
+
if err != nil {
|
50
|
+
return errors.WithStack(err)
|
51
|
+
}
|
52
|
+
defer httpResp.Body.Close()
|
53
|
+
|
54
|
+
responses[i].StatusCode = httpResp.StatusCode
|
55
|
+
|
56
|
+
buf, err := io.ReadAll(httpResp.Body)
|
57
|
+
if err != nil {
|
58
|
+
return errors.WithStack(err)
|
59
|
+
}
|
60
|
+
|
61
|
+
responses[i].Body = buf
|
62
|
+
responses[i].Header = map[string][]string{}
|
63
|
+
|
64
|
+
for key, values := range httpResp.Header {
|
65
|
+
responses[i].Header[key] = append(responses[i].Header[key], values...)
|
66
|
+
}
|
67
|
+
|
68
|
+
return nil
|
69
|
+
})
|
70
|
+
}
|
71
|
+
|
72
|
+
if err := g.Wait(); err != nil {
|
73
|
+
return []Response{}, errors.WithStack(err)
|
74
|
+
}
|
75
|
+
|
76
|
+
return responses, nil
|
77
|
+
}
|
@@ -0,0 +1,120 @@
|
|
1
|
+
package main_test
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/jarcoal/httpmock"
|
5
|
+
"github.com/stretchr/testify/assert"
|
6
|
+
"github.com/sue445/funnel_http"
|
7
|
+
"net/http"
|
8
|
+
"testing"
|
9
|
+
)
|
10
|
+
|
11
|
+
func TestRunRequests(t *testing.T) {
|
12
|
+
httpmock.Activate()
|
13
|
+
t.Cleanup(httpmock.DeactivateAndReset)
|
14
|
+
|
15
|
+
httpmock.RegisterResponder("GET", "http://example.com/1",
|
16
|
+
func(req *http.Request) (*http.Response, error) {
|
17
|
+
resp := httpmock.NewStringResponse(200, "GET http://example.com/1")
|
18
|
+
|
19
|
+
resp.Header.Set("Content-Type", "text/plain")
|
20
|
+
|
21
|
+
for key, values := range req.Header {
|
22
|
+
for _, value := range values {
|
23
|
+
resp.Header.Add(key, value)
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
return resp, nil
|
28
|
+
})
|
29
|
+
|
30
|
+
httpmock.RegisterResponder("GET", "http://example.com/2",
|
31
|
+
func(req *http.Request) (*http.Response, error) {
|
32
|
+
resp := httpmock.NewStringResponse(200, "GET http://example.com/2")
|
33
|
+
|
34
|
+
resp.Header.Set("Content-Type", "text/plain")
|
35
|
+
|
36
|
+
for key, values := range req.Header {
|
37
|
+
for _, value := range values {
|
38
|
+
resp.Header.Add(key, value)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
return resp, nil
|
43
|
+
})
|
44
|
+
|
45
|
+
tests := []struct {
|
46
|
+
name string
|
47
|
+
requests []main.Request
|
48
|
+
expected []main.Response
|
49
|
+
}{
|
50
|
+
{
|
51
|
+
name: "1 request",
|
52
|
+
requests: []main.Request{
|
53
|
+
{
|
54
|
+
Method: "GET",
|
55
|
+
URL: "http://example.com/1",
|
56
|
+
Header: map[string][]string{
|
57
|
+
"X-My-Request-Header": {"a", "b"},
|
58
|
+
},
|
59
|
+
},
|
60
|
+
},
|
61
|
+
expected: []main.Response{
|
62
|
+
{
|
63
|
+
StatusCode: 200,
|
64
|
+
Body: []byte("GET http://example.com/1"),
|
65
|
+
Header: map[string][]string{
|
66
|
+
"Content-Type": {"text/plain"},
|
67
|
+
"X-My-Request-Header": {"a", "b"},
|
68
|
+
},
|
69
|
+
},
|
70
|
+
},
|
71
|
+
},
|
72
|
+
{
|
73
|
+
name: "multiple requests",
|
74
|
+
requests: []main.Request{
|
75
|
+
{
|
76
|
+
Method: "GET",
|
77
|
+
URL: "http://example.com/1",
|
78
|
+
Header: map[string][]string{
|
79
|
+
"X-My-Request-Header": {"a", "b"},
|
80
|
+
},
|
81
|
+
},
|
82
|
+
{
|
83
|
+
Method: "GET",
|
84
|
+
URL: "http://example.com/2",
|
85
|
+
Header: map[string][]string{
|
86
|
+
"X-My-Request-Header": {"c", "d"},
|
87
|
+
},
|
88
|
+
},
|
89
|
+
},
|
90
|
+
expected: []main.Response{
|
91
|
+
{
|
92
|
+
StatusCode: 200,
|
93
|
+
Body: []byte("GET http://example.com/1"),
|
94
|
+
Header: map[string][]string{
|
95
|
+
"Content-Type": {"text/plain"},
|
96
|
+
"X-My-Request-Header": {"a", "b"},
|
97
|
+
},
|
98
|
+
},
|
99
|
+
{
|
100
|
+
StatusCode: 200,
|
101
|
+
Body: []byte("GET http://example.com/2"),
|
102
|
+
Header: map[string][]string{
|
103
|
+
"Content-Type": {"text/plain"},
|
104
|
+
"X-My-Request-Header": {"c", "d"},
|
105
|
+
},
|
106
|
+
},
|
107
|
+
},
|
108
|
+
},
|
109
|
+
}
|
110
|
+
|
111
|
+
httpClient := http.Client{}
|
112
|
+
for _, tt := range tests {
|
113
|
+
t.Run(tt.name, func(t *testing.T) {
|
114
|
+
actual, err := main.RunRequests(&httpClient, tt.requests)
|
115
|
+
if assert.NoError(t, err) {
|
116
|
+
assert.Equal(t, tt.expected, actual)
|
117
|
+
}
|
118
|
+
})
|
119
|
+
}
|
120
|
+
}
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FunnelHttp
|
4
|
+
class Client
|
5
|
+
# @!attribute default_request_header
|
6
|
+
# @return [Hash{String => String, Array<String>}]
|
7
|
+
attr_accessor :default_request_header
|
8
|
+
|
9
|
+
# @param default_request_header [Hash{String => String, Array<String>}]
|
10
|
+
def initialize(default_request_header: {})
|
11
|
+
@default_request_header = {"User-Agent" => USER_AGENT}.merge(default_request_header)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add header to {default_request_header}
|
15
|
+
# @param name [String] Header name
|
16
|
+
# @param value [String, Array<String>] Header value
|
17
|
+
#
|
18
|
+
# @return [Hash{String => String, Array<String>}] {default_request_header} after adding header
|
19
|
+
def add_default_request_header(name, value)
|
20
|
+
default_request_header.merge!(name => value)
|
21
|
+
end
|
22
|
+
|
23
|
+
# perform HTTP requests in parallel
|
24
|
+
#
|
25
|
+
# @overload perform(requests)
|
26
|
+
# @param requests [Array<Hash{Symbol => Object}>] `Array` of following `Hash`
|
27
|
+
# @option requests :method [String, Symbol] **[required]** Request method (e.g. `:get`, `"POST"`)
|
28
|
+
# @option requests :url [String] **[required]** Request url
|
29
|
+
# @option requests :header [Hash{String => String, Array<String>}, nil] Request header
|
30
|
+
#
|
31
|
+
# @overload perform(request)
|
32
|
+
# @param request [Hash{Symbol => Object}]
|
33
|
+
# @option request :method [String, Symbol] **[required]** Request method (e.g. `:get`, `"POST"`)
|
34
|
+
# @option request :url [String] **[required]** Request url
|
35
|
+
# @option request :header [Hash{String => String, Array<String>}, nil] Request header
|
36
|
+
#
|
37
|
+
# @return [Array<Hash<Symbol => Object>>] `Array` of following `Hash`
|
38
|
+
# @return [Integer] `:status_code`
|
39
|
+
# @return [String] `:body` Response body
|
40
|
+
# @return [Hash{String => Array<String>}] `:header` Response header
|
41
|
+
def perform(requests)
|
42
|
+
ext_client.run_requests(normalize_requests(requests))
|
43
|
+
end
|
44
|
+
|
45
|
+
# @overload normalize_requests(requests)
|
46
|
+
# @param requests [Array<Hash{Symbol => Object}>] `Array` of following `Hash`
|
47
|
+
# @option requests :method [String, Symbol] **[required]** Request method (e.g. `:get`, `"POST"`)
|
48
|
+
# @option requests :url [String] **[required]** Request url
|
49
|
+
# @option requests :header [Hash{String => String, Array<String>}, nil] Request header
|
50
|
+
#
|
51
|
+
# @overload normalize_requests(request)
|
52
|
+
# @param request [Hash{Symbol => Object}]
|
53
|
+
# @option request :method [String, Symbol] **[required]** Request method (e.g. `:get`, `"POST"`)
|
54
|
+
# @option request :url [String] **[required]** Request url
|
55
|
+
# @option request :header [Hash{String => String, Array<String>}, nil] Request header
|
56
|
+
#
|
57
|
+
# @return [Array<Hash{Symbol => Object}>] `Array` of following `Hash`
|
58
|
+
# @return [String] `:method` Request method (e.g. `"POST"`)
|
59
|
+
# @return [String] `:url` Request url
|
60
|
+
# @return [Hash{String => Array<String>}] `:header` Request header
|
61
|
+
def normalize_requests(arg)
|
62
|
+
requests =
|
63
|
+
case arg
|
64
|
+
when Array
|
65
|
+
arg
|
66
|
+
when Hash
|
67
|
+
[arg]
|
68
|
+
else
|
69
|
+
raise ArgumentError, "#{arg} must be Array or Hash"
|
70
|
+
end
|
71
|
+
|
72
|
+
requests.map do |request|
|
73
|
+
raise ArgumentError, "#{arg} contains something other than Hash" unless request.is_a?(Hash)
|
74
|
+
|
75
|
+
raise ArgumentError, "#{arg} key does not contain all :method and :url" if !request.key?(:method) || !request.key?(:url)
|
76
|
+
|
77
|
+
{
|
78
|
+
url: request[:url].to_s,
|
79
|
+
method: request[:method].to_s.upcase,
|
80
|
+
header: normalize_header(request[:header]),
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# @param header [Hash{String => String, Array<String>}, nil] Request header
|
88
|
+
# @return [Hash{String => Array<String>}] Request header
|
89
|
+
def normalize_header(header)
|
90
|
+
full_header =
|
91
|
+
if header
|
92
|
+
default_request_header.dup.merge(header)
|
93
|
+
else
|
94
|
+
default_request_header.dup
|
95
|
+
end
|
96
|
+
|
97
|
+
# Workaround for Ruby::UnannotatedEmptyCollection on steep 1.9.0+
|
98
|
+
result = {} #: strict_header
|
99
|
+
|
100
|
+
full_header.each_with_object(result) do |(k, v), hash|
|
101
|
+
# FIXME: Fails `steep check` when use Array(v)...
|
102
|
+
# hash[k] = Array(v)
|
103
|
+
hash[k] = v.is_a?(Array) ? v : Array(v)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [FunnelHttp::Ext::Client]
|
108
|
+
def ext_client
|
109
|
+
@ext_client ||= FunnelHttp::Ext::Client.new
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/funnel_http.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "funnel_http/version"
|
4
|
+
require_relative "funnel_http/client"
|
5
|
+
require_relative "funnel_http/ext"
|
6
|
+
require_relative "funnel_http/funnel_http"
|
7
|
+
|
8
|
+
module FunnelHttp
|
9
|
+
class Error < StandardError; end
|
10
|
+
|
11
|
+
USER_AGENT = "funnel_http/#{FunnelHttp::VERSION} (+https://github.com/sue445/funnel_http)"
|
12
|
+
|
13
|
+
# Your code goes here...
|
14
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
---
|
2
|
+
path: ".gem_rbs_collection"
|
3
|
+
gems:
|
4
|
+
- name: base64
|
5
|
+
version: '0'
|
6
|
+
source:
|
7
|
+
type: stdlib
|
8
|
+
- name: cgi
|
9
|
+
version: '0'
|
10
|
+
source:
|
11
|
+
type: stdlib
|
12
|
+
- name: diff-lcs
|
13
|
+
version: '1.5'
|
14
|
+
source:
|
15
|
+
type: git
|
16
|
+
name: ruby/gem_rbs_collection
|
17
|
+
revision: 0a6ea105a0afc7eaee4494585a7775f47eea6145
|
18
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
19
|
+
repo_dir: gems
|
20
|
+
- name: fileutils
|
21
|
+
version: '0'
|
22
|
+
source:
|
23
|
+
type: stdlib
|
24
|
+
- name: logger
|
25
|
+
version: '0'
|
26
|
+
source:
|
27
|
+
type: stdlib
|
28
|
+
- name: monitor
|
29
|
+
version: '0'
|
30
|
+
source:
|
31
|
+
type: stdlib
|
32
|
+
- name: rack
|
33
|
+
version: '2.2'
|
34
|
+
source:
|
35
|
+
type: git
|
36
|
+
name: ruby/gem_rbs_collection
|
37
|
+
revision: 0a6ea105a0afc7eaee4494585a7775f47eea6145
|
38
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
39
|
+
repo_dir: gems
|
40
|
+
- name: rake
|
41
|
+
version: '13.0'
|
42
|
+
source:
|
43
|
+
type: git
|
44
|
+
name: ruby/gem_rbs_collection
|
45
|
+
revision: 0a6ea105a0afc7eaee4494585a7775f47eea6145
|
46
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
47
|
+
repo_dir: gems
|
48
|
+
- name: sinatra
|
49
|
+
version: '4.0'
|
50
|
+
source:
|
51
|
+
type: git
|
52
|
+
name: ruby/gem_rbs_collection
|
53
|
+
revision: 0a6ea105a0afc7eaee4494585a7775f47eea6145
|
54
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
55
|
+
repo_dir: gems
|
56
|
+
- name: stringio
|
57
|
+
version: '0'
|
58
|
+
source:
|
59
|
+
type: stdlib
|
60
|
+
- name: tempfile
|
61
|
+
version: '0'
|
62
|
+
source:
|
63
|
+
type: stdlib
|
64
|
+
- name: uri
|
65
|
+
version: '0'
|
66
|
+
source:
|
67
|
+
type: stdlib
|
68
|
+
gemfile_lock_path: Gemfile.lock
|
data/rbs_collection.yaml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Download sources
|
2
|
+
sources:
|
3
|
+
- type: git
|
4
|
+
name: ruby/gem_rbs_collection
|
5
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
6
|
+
revision: main
|
7
|
+
repo_dir: gems
|
8
|
+
|
9
|
+
# You can specify local directories as sources also.
|
10
|
+
# - type: local
|
11
|
+
# path: path/to/your/local/repository
|
12
|
+
|
13
|
+
# A directory to install the downloaded RBSs
|
14
|
+
path: .gem_rbs_collection
|
15
|
+
|
16
|
+
gems:
|
17
|
+
- name: rbs
|
18
|
+
ignore: true
|
19
|
+
- name: steep
|
20
|
+
ignore: true
|
21
|
+
- name: yard
|
22
|
+
ignore: true
|
23
|
+
- name: funnel_http
|
24
|
+
ignore: true
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module FunnelHttp
|
2
|
+
class Client
|
3
|
+
@ext_client: FunnelHttp::Ext::Client
|
4
|
+
|
5
|
+
attr_accessor default_request_header: fuzzy_header
|
6
|
+
|
7
|
+
def initialize: (?default_request_header: fuzzy_header) -> void
|
8
|
+
|
9
|
+
def add_default_request_header: (String name, String | Array[String] value) -> fuzzy_header
|
10
|
+
|
11
|
+
def perform: (fuzzy_request | Array[fuzzy_request] requests) -> Array[response]
|
12
|
+
|
13
|
+
def normalize_requests: (fuzzy_request | Array[fuzzy_request] arg) -> Array[strict_request]
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def normalize_header: (fuzzy_header? header) -> strict_header
|
18
|
+
|
19
|
+
def ext_client: () -> FunnelHttp::Ext::Client
|
20
|
+
end
|
21
|
+
end
|
data/sig/funnel_http.rbs
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module FunnelHttp
|
2
|
+
VERSION: String
|
3
|
+
USER_AGENT: String
|
4
|
+
|
5
|
+
class Error < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
type fuzzy_header = Hash[String, String | Array[String]]
|
9
|
+
|
10
|
+
type fuzzy_request = {
|
11
|
+
method: String | Symbol,
|
12
|
+
url: String,
|
13
|
+
header: fuzzy_header?
|
14
|
+
}
|
15
|
+
|
16
|
+
type strict_header = Hash[String, Array[String]]
|
17
|
+
|
18
|
+
type strict_request = {
|
19
|
+
method: String,
|
20
|
+
url: String,
|
21
|
+
header: strict_header
|
22
|
+
}
|
23
|
+
|
24
|
+
type response = {
|
25
|
+
status_code: Integer,
|
26
|
+
body: String,
|
27
|
+
header: strict_header
|
28
|
+
}
|
29
|
+
|
30
|
+
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
31
|
+
end
|