Skip to content
This repository was archived by the owner on Sep 4, 2020. It is now read-only.

Commit 4192d86

Browse files
committed
added multiple responses per endpoint
1 parent 7177822 commit 4192d86

3 files changed

Lines changed: 156 additions & 76 deletions

File tree

README.md

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[![Build Status](https://travis-ci.org/schigh/fony.svg?branch=master)](https://travis-ci.org/schigh/fony)
2-
32
[![Go Report Card](https://goreportcard.com/badge/github.com/schigh/fony)](https://goreportcard.com/report/github.com/schigh/fony)
43

54
# fony
@@ -17,46 +16,44 @@ Create a json file with a list of your endpoints like so:
1716
{
1817
"url": "/foo/bar",
1918
"verb": "GET",
20-
"response_payload": {
21-
"foo": "bar",
22-
"fizz": "buzz"
23-
},
24-
"response_code": 200
25-
},
26-
{
27-
"url": "/herp/derp",
28-
"verb": "POST",
29-
"response_payload": [
19+
"responses": [
20+
{
21+
"headers": {
22+
"X-Herp": "DDDERP"
23+
},
24+
"payload": {
25+
"foo": "bar",
26+
"fizz": "buzz"
27+
},
28+
"status_code": 200
29+
},
3030
{
31-
"duck": 1,
32-
"cow": true,
33-
"pig": null
31+
"headers": {
32+
"X-Herp": "HHHERP"
33+
},
34+
"payload": {
35+
"foo": "bark",
36+
"fizz": "moo"
37+
},
38+
"status_code": 201
3439
}
35-
],
36-
"response_code": 200
37-
},
38-
{
39-
"url": "/i/am/a/teapot",
40-
"verb": "GET",
41-
"response_payload": {},
42-
"response_code": 418
40+
]
4341
}
4442
]
4543
}
4644
```
4745

46+
For each endpoint, the url and verb are required. There must also be at least one response per endpoint as well.
47+
48+
If you have multiple responses for the same endpoint, you can specify which one to return in the headers of your request. To do this, set a header called `X-Fony-Index` to the index of the response you wish to return. If you pass an index out of range, the response at index `0` will be returned.
49+
4850
Use `docker-compose` to run the `fony` command. Be sure to set the `GOBIN` environment variable to `/go/bin`.
4951
See the `docker-compose.yml` file in this repository for an example.
5052

5153
## Things that need done
5254
- All the tests
5355

54-
## Known bugs
55-
Headers are not parsing properly at the moment
56-
57-
## Future enhancements
58-
Right now there can only be one call per endpoint/http verb combo.
59-
We need to be able to allow multiple responses for each endpoint/http verb.
60-
61-
One proposed solution would be push a list of response payloads to the endpoint, and return them sequentially.
62-
I will revisit this if the need arises.
56+
## Caveats
57+
This project is meant to run in a Docker container. The necessary go dependencies will be installed when the Dockerfile is built. If you want to run this program locally, you will need to install the following dependencies via `go get`:
58+
- `github.com/sirupsen/logrus`
59+
- `goji.io`

fony.go

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,40 @@ import (
88
"net/http"
99
"os"
1010
"path/filepath"
11+
"strconv"
1112
"strings"
1213

1314
log "github.com/sirupsen/logrus"
1415
"goji.io"
1516
"goji.io/pat"
1617
)
1718

18-
// PhonyEndpoint represents a single endpoint served by fony
19-
type PhonyEndpoint struct {
20-
URL string `json:"url"`
21-
Verb string `json:"verb"`
22-
ResponseHeaders map[string]string `json:"response_headers"`
23-
ResponsePayload interface{} `json:"response_payload"`
24-
ResponseCode int `json:"response_code"`
19+
// FonyResponse contains the data returned in a specific response
20+
type FonyResponse struct {
21+
Headers map[string]string `json:"headers"`
22+
Payload interface{} `json:"payload"`
23+
StatusCode int `json:"status_code"`
2524
}
2625

27-
// PhonySuite encloses the suite of endpoints served by fony
28-
type PhonySuite struct {
26+
// FonyEndpoint represents a single endpoint served by fony
27+
type FonyEndpoint struct {
28+
URL string `json:"url"`
29+
Verb string `json:"verb"`
30+
Responses []FonyResponse `json:"responses"`
31+
}
32+
33+
// FonySuite encloses the suite of endpoints served by fony
34+
type FonySuite struct {
2935
GlobalHeaders map[string]string `json:"global_headers"`
30-
Endpoints []*PhonyEndpoint `json:"endpoints"`
36+
Endpoints []FonyEndpoint `json:"endpoints"`
3137
}
3238

3339
// patFunction is an alias to pat http verb functions
3440
type patFunction func(url string) *pat.Pattern
3541

42+
// FonyPayloadIndexHeader is the index of the payload we wish to see for a given endpoint
43+
const FonyPayloadIndexHeader = "X-Fony-Index"
44+
3645
var (
3746
suiteFile string
3847
)
@@ -41,7 +50,8 @@ func init() {
4150
flag.StringVar(&suiteFile, "f", "./suite.json", "Absolute path to the fony suite file")
4251
}
4352

44-
func setup() (*PhonySuite, bool) {
53+
// setup prepares the suite for service
54+
func setup() (*FonySuite, bool) {
4555
flag.Parse()
4656

4757
fileInfo, err := os.Stat(suiteFile)
@@ -60,7 +70,7 @@ func setup() (*PhonySuite, bool) {
6070
return nil, false
6171
}
6272

63-
suite := &PhonySuite{}
73+
suite := &FonySuite{}
6474
ext := filepath.Ext(suiteFile)
6575
switch ext {
6676
case ".json":
@@ -76,19 +86,8 @@ func setup() (*PhonySuite, bool) {
7686
return suite, true
7787
}
7888

79-
func register(suite *PhonySuite) (*goji.Mux, bool) {
80-
mux := goji.NewMux()
81-
for _, ep := range suite.Endpoints {
82-
if err := processEndpoint(ep, mux, suite.GlobalHeaders); err != nil {
83-
log.Errorf("Error registering endpoint %s: %+v", ep.URL, err)
84-
return nil, false
85-
}
86-
}
87-
88-
return mux, true
89-
}
90-
91-
func getPatFunction(ep *PhonyEndpoint) (patFunction, error) {
89+
// getPatFunction will get the appropriate pat function based on HTTP verb
90+
func getPatFunction(ep FonyEndpoint) (patFunction, error) {
9291
verb := strings.ToUpper(ep.Verb)
9392
switch verb {
9493
case "GET":
@@ -110,34 +109,90 @@ func getPatFunction(ep *PhonyEndpoint) (patFunction, error) {
110109
return nil, fmt.Errorf("unknown HTTP method: %s", verb)
111110
}
112111

113-
func processEndpoint(ep *PhonyEndpoint, mux *goji.Mux, globals map[string]string) error {
112+
func errOut(w http.ResponseWriter) {
113+
w.Header().Set("Content-Type", "text/plain")
114+
w.WriteHeader(500)
115+
w.Write([]byte("Fony failed to process this endpoint. Check your logs to find the root cause"))
116+
}
117+
118+
// processEndpoint will set each endpoint and handler in the muxxer
119+
func processEndpoint(ep FonyEndpoint, mux *goji.Mux, globals map[string]string) error {
114120
f, err := getPatFunction(ep)
115121
if err != nil {
116122
return err
117123
}
118124

119125
mux.HandleFunc(f(ep.URL), func(w http.ResponseWriter, r *http.Request) {
126+
// by default, all responses return this header. It can be overwritten in the global
127+
// headers or the endpoint-specific header
120128
w.Header().Set("Content-Type", "application/json")
121129
for k := range globals {
122130
w.Header().Set(k, globals[k])
123131
}
124-
for k := range ep.ResponseHeaders {
125-
w.Header().Set(k, ep.ResponseHeaders[k])
132+
133+
// get the index header if set
134+
var index uint64
135+
indexHeader := r.Header.Get(FonyPayloadIndexHeader)
136+
if indexHeader != "" {
137+
index, err = strconv.ParseUint(indexHeader, 10, 8)
138+
if err != nil {
139+
log.Errorf("header index parse error: %+v", err)
140+
errOut(w)
141+
return
142+
}
126143
}
127144

128-
data, err := json.Marshal(ep.ResponsePayload)
129-
if err != nil {
130-
log.Errorf("Payload marshal error: %+v", err)
145+
var response *FonyResponse
146+
numResponses := len(ep.Responses)
147+
148+
if numResponses > int(index) {
149+
response = &ep.Responses[index]
150+
} else if numResponses > 0 {
151+
response = &ep.Responses[0]
152+
} else {
153+
// there must be at least one response
154+
log.Error("There must be at least one response per endpoint")
155+
errOut(w)
131156
return
132157
}
133158

134-
w.WriteHeader(ep.ResponseCode)
159+
// specific endpoint headers will override globals if set
160+
for k := range response.Headers {
161+
w.Header().Set(k, response.Headers[k])
162+
}
163+
164+
var data []byte
165+
if response.Payload != nil {
166+
data, err = json.Marshal(response.Payload)
167+
if err != nil {
168+
log.Errorf("payload marshal error: %+v", err)
169+
return
170+
}
171+
}
172+
173+
if response.StatusCode == 0 {
174+
response.StatusCode = 200
175+
}
176+
w.WriteHeader(response.StatusCode)
135177
w.Write(data)
136178
})
137179

138180
return nil
139181
}
140182

183+
// register will register each endpoint in the muxxer
184+
func register(suite *FonySuite) (*goji.Mux, bool) {
185+
mux := goji.NewMux()
186+
for _, ep := range suite.Endpoints {
187+
if err := processEndpoint(ep, mux, suite.GlobalHeaders); err != nil {
188+
log.Errorf("Error registering endpoint %s: %+v", ep.URL, err)
189+
return nil, false
190+
}
191+
}
192+
193+
return mux, true
194+
}
195+
141196
func main() {
142197
suite, ok := setup()
143198
if !ok {

sample/sample.json

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,57 @@
66
{
77
"url": "/foo/bar",
88
"verb": "GET",
9-
"response_payload": {
10-
"foo": "bar",
11-
"fizz": "buzz"
12-
},
13-
"response_code": 200
9+
"responses": [
10+
{
11+
"headers": {
12+
"X-Herp": "DDDERP"
13+
},
14+
"payload": {
15+
"foo": "bar",
16+
"fizz": "buzz"
17+
},
18+
"status_code": 200
19+
},
20+
{
21+
"headers": {
22+
"X-Herp": "HHHERP"
23+
},
24+
"payload": {
25+
"foo": "bark",
26+
"fizz": "moo"
27+
},
28+
"status_code": 201
29+
}
30+
]
1431
},
1532
{
1633
"url": "/herp/derp",
1734
"verb": "POST",
18-
"response_payload": [
35+
"responses": [
1936
{
20-
"duck": 1,
21-
"cow": true,
22-
"pig": null
37+
"headers": {
38+
"X-Fony": "override"
39+
},
40+
"payload": [
41+
{
42+
"duck": 1,
43+
"cow": true,
44+
"pig": null
45+
}
46+
],
47+
"status_code": 200
2348
}
24-
],
25-
"response_code": 200
49+
]
2650
},
2751
{
2852
"url": "/i/am/a/teapot",
2953
"verb": "GET",
30-
"response_payload": {},
31-
"response_code": 418
54+
"responses": [
55+
{
56+
"payload": null,
57+
"status_code": 418
58+
}
59+
]
3260
}
3361
]
3462
}

0 commit comments

Comments
 (0)