Skip to content

Commit 0e5926d

Browse files
committed
feat(Responsefile): implement Responsefile for multiple responses
1 parent c47a526 commit 0e5926d

File tree

7 files changed

+199
-32
lines changed

7 files changed

+199
-32
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ brew install jamescun/formulas/httplog
2121
## Usage
2222

2323
```
24-
httplog v1.0.2
24+
httplog v1.0.3
2525
2626
httplog is a command line tool that launches a local HTTP server that logs all
2727
requests it receives, replying with a canned response.
@@ -38,6 +38,8 @@ Options:
3838
to all requests (default 200)
3939
--response-header <X=Y> configure one or more headers to be sent in the
4040
response, may be specified more than once
41+
--responses <file> configure multiple responses using a
42+
Responsefile (recommended)
4143
--json log all requests as JSON rather than human
4244
readable text
4345
--tls-self-cert enable TLS with a self-signed certificate
@@ -66,3 +68,9 @@ Hello World
6668
```
6769

6870
If you would prefer JSON output, rather than human-readable text, use the `--json` option.
71+
72+
## Responsefile
73+
74+
A Responsefile is a YAML file that configures HTTPLog with multiple canned responses, as well as defaults and error handling.
75+
76+
See [Responsefile.example](Responsefile.example) for an example Responsefile.

Responsefile.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
responses:
2+
- method: GET
3+
path: /foo/bar
4+
status: 200
5+
headers:
6+
"Content-Type": [ "application/json" ]
7+
body: >-
8+
{"foo": "bar"}
9+
10+
11+
notFound:
12+
status: 404
13+
headers:
14+
"Content-Type": [ "application/json" ]
15+
body: >-
16+
{"error":{"code": "NOT_FOUND"}}
17+
18+
methodNotAllowed:
19+
status: 405
20+
headers:
21+
"Content-Type": [ "application/json" ]
22+
body: >-
23+
{"error":{"code": "METHOD_NOT_ALLOWED"}}

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ module github.com/jamescun/httplog
22

33
go 1.19
44

5-
require github.com/spf13/pflag v1.0.5
5+
require (
6+
github.com/go-chi/chi/v5 v5.0.10
7+
github.com/spf13/pflag v1.0.5
8+
gopkg.in/yaml.v3 v3.0.1
9+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1+
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
2+
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
13
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
24
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
5+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
6+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
7+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
8+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

httplog/server.go

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,9 @@ import (
1010
)
1111

1212
type Server struct {
13-
// ResponseCode is the HTTP Status Code sent in response to all requests,
14-
// if not set, HTTP 200 is used.
15-
ResponseCode int
16-
17-
// ResponseBody is the contents sent in response to all requests, if not
18-
// set, no response body is used.
19-
ResponseBody []byte
20-
21-
// ResponseHEaders are additional HTTP headers to be sent in response to
22-
// all requests.
23-
ResponseHeaders http.Header
13+
// Handler is the HTTP Handler executed for all requests. If not
14+
// configured, an HTTP 200 will be sent in response to all requests.
15+
Handler http.Handler
2416

2517
requests chan *Request
2618
}
@@ -66,20 +58,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6658

6759
s.requests <- req
6860

69-
if len(s.ResponseHeaders) > 0 {
70-
for key, values := range s.ResponseHeaders {
71-
for _, value := range values {
72-
r.Header.Set(key, value)
73-
}
74-
}
75-
}
76-
77-
if s.ResponseCode > 0 {
78-
w.WriteHeader(s.ResponseCode)
79-
}
80-
81-
if len(s.ResponseBody) > 0 {
82-
w.Write(s.ResponseBody)
61+
if s.Handler != nil {
62+
s.Handler.ServeHTTP(w, r)
63+
} else {
64+
w.WriteHeader(http.StatusOK)
8365
}
8466
}
8567

main.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/spf13/pflag"
1717

1818
"github.com/jamescun/httplog/httplog"
19+
"github.com/jamescun/httplog/responses"
1920
)
2021

2122
// Version is the semantic release version of this build of httplog.
@@ -26,6 +27,7 @@ var (
2627
responseBody = pflag.String("response", "", "configure the canned body sent in response to all requests")
2728
responseCode = pflag.Int("response-code", 200, "configure the HTTP status code sent in response requests")
2829
responseHeader = pflag.StringArray("response-header", nil, "configure one or more headers to be sent in the response")
30+
responseFile = pflag.String("responses", "", "ponfigure multiple responses using a Responsefile")
2931
logJSON = pflag.Bool("json", false, "log all requests as JSON rather than human readable text")
3032
tlsSelfCert = pflag.Bool("tls-self-cert", false, "enable TLS with a self-signed certificate")
3133
)
@@ -47,6 +49,8 @@ Options:
4749
to all requests (default 200)
4850
--response-header <X=Y> configure one or more headers to be sent in the
4951
response, may be specified more than once
52+
--responses <file> configure multiple responses using a
53+
Responsefile (recommended)
5054
--json log all requests as JSON rather than human
5155
readable text
5256
--tls-self-cert enable TLS with a self-signed certificate
@@ -58,13 +62,24 @@ func main() {
5862

5963
srv := httplog.NewServer(128)
6064

61-
srv.ResponseCode = *responseCode
65+
if *responseFile != "" {
66+
file, err := responses.ReadFile(*responseFile)
67+
if err != nil {
68+
exitError(2, "could not read Responsefile: %s", err)
69+
}
6270

63-
if *responseBody != "" {
64-
srv.ResponseBody = []byte(*responseBody)
65-
}
71+
srv.Handler = file.Handler()
72+
} else {
73+
r := &responses.File{
74+
NotFound: &responses.Response{
75+
Status: *responseCode,
76+
Headers: httplog.ParseHeaders(*responseHeader),
77+
Body: *responseBody,
78+
},
79+
}
6680

67-
srv.ResponseHeaders = httplog.ParseHeaders(*responseHeader)
81+
srv.Handler = r.Handler()
82+
}
6883

6984
if *logJSON {
7085
go httplog.JSONLogger(os.Stdout, srv.Requests())

responses/responses.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package responses
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"os"
7+
8+
"github.com/go-chi/chi/v5"
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
type File struct {
13+
// Responses are all the pre-defined responses that HTTPLog will be
14+
// configured to reply to. If none match the request, Default will be used.
15+
Responses []*Response `yaml:"responses"`
16+
17+
// Headers are HTTP Headers that are to be sent in response to all
18+
// requests.
19+
Headers http.Header `yaml:"headers"`
20+
21+
// NotFound is the default response sent by HTTPLog when no Response was
22+
// matched for a request.
23+
NotFound *Response `yaml:"notFound"`
24+
25+
// MethodNotAllowed is the default response sent by HTTPLog when a Response
26+
// was matched but not for the method of the request.
27+
MethodNotAllowed *Response `yaml:"methodNotAllowed"`
28+
}
29+
30+
// ReadFile reads and unmarshals a Responsefile from a YAML file on disk.
31+
func ReadFile(path string) (*File, error) {
32+
file, err := os.OpenFile(path, os.O_RDONLY, 0)
33+
if err != nil {
34+
return nil, err
35+
}
36+
defer file.Close()
37+
38+
var cfg File
39+
40+
err = yaml.NewDecoder(file).Decode(&cfg)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return &cfg, nil
46+
}
47+
48+
// Handler builds the response routes configured on File into a HTTP router.
49+
func (f *File) Handler() http.Handler {
50+
r := chi.NewRouter()
51+
52+
if len(f.Headers) > 0 {
53+
r.Use(func(next http.Handler) http.Handler {
54+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55+
for key, values := range f.Headers {
56+
for _, value := range values {
57+
w.Header().Set(key, value)
58+
}
59+
}
60+
61+
next.ServeHTTP(w, r)
62+
})
63+
})
64+
}
65+
66+
if f.NotFound != nil {
67+
r.NotFound(f.NotFound.handlerFunc())
68+
}
69+
70+
if f.MethodNotAllowed != nil {
71+
r.MethodNotAllowed(f.MethodNotAllowed.handlerFunc())
72+
}
73+
74+
for _, response := range f.Responses {
75+
if response.Method != "" {
76+
r.Method(response.Method, response.Path, response.handlerFunc())
77+
} else {
78+
r.Handle(response.Path, response.handlerFunc())
79+
}
80+
}
81+
82+
return r
83+
}
84+
85+
type Response struct {
86+
// Method is the HTTP Method verb to match this request, or all requests if
87+
// not configured.
88+
Method string `yaml:"method"`
89+
90+
// Path is the URL Path where this response/ will be made available by
91+
// HTTPLog. Internally, HTTPLog uses the Chi router, which supports
92+
// parameters and regular expressions. Parameters are currently unused.
93+
Path string `yaml:"path"`
94+
95+
// Status is the HTTP Status Code returned by this Response, or HTTP 200
96+
// if not configured.
97+
Status int `yaml:"status"`
98+
99+
// Headers are HTTP Headers that are sent in response to this request only.
100+
Headers http.Header `yaml:"headers"`
101+
102+
// Body is the contents of the response body.
103+
Body string `yaml:"body"`
104+
105+
// File is like Body, except reads from a file path.
106+
File string `yaml:"file"`
107+
}
108+
109+
func (res *Response) handlerFunc() http.HandlerFunc {
110+
return func(w http.ResponseWriter, r *http.Request) {
111+
for key, values := range res.Headers {
112+
for _, value := range values {
113+
w.Header().Set(key, value)
114+
}
115+
}
116+
117+
if res.Status > 0 {
118+
w.WriteHeader(res.Status)
119+
} else {
120+
w.WriteHeader(http.StatusOK)
121+
}
122+
123+
if res.Body != "" {
124+
io.WriteString(w, res.Body)
125+
} else if res.File != "" {
126+
http.ServeFile(w, r, res.File)
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)