├── .gitignore ├── .travis.yml ├── LICENCE ├── Makefile ├── README.md ├── api.go ├── api_test.go ├── cmd └── apidemic │ └── main.go ├── fixtures ├── response.json ├── sample.json └── sample_request.json ├── json.go ├── json_test.go ├── tags.go └── tags_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9 4 | - "1.10" 5 | install: 6 | - go get -t ./... 7 | script: 8 | - make test -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Geofrey Ernest 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test : 2 | @go test -cover 3 | 4 | deps: 5 | @go get github.com/mitchellh/gox 6 | 7 | dist: 8 | @gox -output="bin/{{.Dir}}v$(VERSION)_{{.OS}}_{{.Arch}}/{{.Dir}}" ./cmd/apidemic -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apidemic [![Build Status](https://travis-ci.org/gernest/apidemic.svg)](https://travis-ci.org/gernest/apidemic) 2 | 3 | Apidemic is a service for generating fake JSON response. You first register the sample JSON response, and apidemic will serve that response with random fake data. 4 | 5 | This is experimental, so take it with a grain of salt. 6 | 7 | # Motivation 8 | I got bored with hardcoding the sample json api response in tests. If you know golang, you can benefit by using the library, I have included a router that you can use to run disposable servers in your tests. 9 | 10 | # Installation 11 | 12 | You can download the binaries for your respective operating system [Download apidemic](https://github.com/gernest/apidemic/releases/latest) 13 | 14 | Then put the downloaded binary somewhere in your system path. 15 | 16 | Alternatively, if you have golang installed 17 | 18 | go get github.com/gernest/apidemic/cmd/apidemic 19 | 20 | 21 | Now you can start the service like this 22 | 23 | apidemic start 24 | 25 | This will run a service at localhost default port is 3000, you can change the port by adding a flag `--port=YOUR_PORT_NUMBER` 26 | 27 | 28 | # How to use 29 | Lets say you expect a response like this 30 | 31 | ```json 32 | { 33 | "name": "anton", 34 | "age": 29, 35 | "nothing": null, 36 | "true": true, 37 | "false": false, 38 | "list": [ 39 | "first", 40 | "second" 41 | ], 42 | "list2": [ 43 | { 44 | "street": "Street 42", 45 | "city": "Stockholm" 46 | }, 47 | { 48 | "street": "Street 42", 49 | "city": "Stockholm" 50 | } 51 | ], 52 | "address": { 53 | "street": "Street 42", 54 | "city": "Stockholm" 55 | }, 56 | "country": { 57 | "name": "Sweden" 58 | } 59 | } 60 | ``` 61 | 62 | 63 | If you have already started apidemic server you can register that response by making a POST request to the `/register` path. Passing the json body of the form. 64 | 65 | ```json 66 | { 67 | "endpoint": "test", 68 | "payload": { 69 | "name: first_name": "anton", 70 | "age: digits_n,max=2": 29, 71 | "nothing:": null, 72 | "true": true, 73 | "false": false, 74 | "list:word,max=3": [ 75 | "first", 76 | "second" 77 | ], 78 | "list2": [ 79 | { 80 | "street:street": "Street 42", 81 | "city:city": "Stockholm" 82 | }, 83 | { 84 | "street": "Street 42", 85 | "city": "Stockholm" 86 | } 87 | ], 88 | "address": { 89 | "street:street": "Street 42", 90 | "city:city": "Stockholm" 91 | }, 92 | "country": { 93 | "name:country": "Sweden" 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | See the annotation tags on the payload. Example if I want to generate full name for a field name I will just add `"name:full_name"`. 100 | 101 | Once your POST request is submitted you are good to ask for the response with fake values. Just make a GET request to the endpoint you registered. 102 | 103 | So every GET call to `/api/test` will return the api response with fake data. 104 | 105 | # Routes 106 | Apidemic server has only three http routes 107 | 108 | ### / 109 | This is the home path. It only renders information about the apidemic server. 110 | 111 | ### /register 112 | This is where you register endpoints. You POST the annotated sample JSON here. The request body should be a json object of signature. 113 | 114 | ```json 115 | { 116 | "endpoint":"my_endpoint", 117 | "payload": { ANNOTATED__SAMPLE_JSON_GOES_HERE }, 118 | } 119 | ``` 120 | 121 | #### /api/{REGISTERED_ENDPOINT_GOES_HERE} 122 | Every GET request on this route will render a fake JSON object for the sample registered in this endpoint. 123 | 124 | #### Other HTTP Methods 125 | In case you need to mimic endpoints which respond to requests other than GET then make sure to add an `http_method` key with the required method name into your API description. 126 | 127 | ```json 128 | { 129 | "endpoint": "test", 130 | "http_method": "POST", 131 | "payload": { 132 | "name: first_name": "anton" 133 | } 134 | } 135 | ``` 136 | 137 | Currently supported HTTP methods are: `OPTIONS`, `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, default is `GET`. Please open an issue if you think there should be others added. 138 | 139 | #### Emulate unexpected responses 140 | Sometimes you need to ensure that your application handles API errors correctly in which case you can add a `response_code_probabilities` field with a map of response codes to probabilities. 141 | ```json 142 | { 143 | "endpoint": "test", 144 | "response_code_probabilities": { 145 | "404": 10, 146 | "503": 5, 147 | "418": 1 148 | }, 149 | "payload": { 150 | "name: first_name": "anton" 151 | } 152 | } 153 | ``` 154 | 155 | With the above configuration there's a 84% chance to get a `200 OK` response. 156 | The server will respond with `404 Not Found` about 1 out of 10 times and with `503 Service Unavailable` 1 out of 20 times. 157 | There's also a 1% chance for the server to claim to be a [Teapot](https://tools.ietf.org/html/rfc2324). 158 | 159 | **Note**: JSON keys must be strings, providing your response codes as integers will not work! 160 | 161 | # Tags 162 | Apidemic uses tags to annotate what kind of fake data to generate and also control different requrements of fake data. 163 | 164 | You add tags to object keys. For instance let's say you have a JSON object `{ "user_name": "gernest"}`. If you want to have a fake username then you can annotate the key by adding user_name tag like this `{ "user_name:user_name": "gernest"}`. 165 | 166 | So JSON keys can be annotated by adding the `:` symbol then followed by comma separated list of tags. The first entry after `:` is for the tag type, the following entries are in the form `key=value` which will be the extra information to fine-tune your fake data. Please see the example above to see how tags are used. 167 | 168 | Apidemic comes shipped with a large number of tags, meaning it is capable to generate a wide range of fake information. 169 | 170 | These are currently available tags to generate different fake data: 171 | 172 | Tag | Details( data generated) 173 | ------|-------- 174 | brand | brand 175 | character | character 176 | characters | characters 177 | characters_n | characters of maximum length n 178 | city | city 179 | color | color 180 | company | company 181 | continent | continent 182 | country | country 183 | credit_card_num | credit card number 184 | currency | currency 185 | currency_code | currency code 186 | day | day 187 | digits | digits 188 | digits_n | digits of maximum number n 189 | domain_name | domain name 190 | domain_zone | domain zone 191 | email_address | email address 192 | email_body | email body 193 | female_first_name | female first name 194 | female_full_name | female full name 195 | female_full_name_with_prefix | female full name with prefix 196 | female_full_name_with_suffix | female full name with suffix 197 | female_last_name | female last name 198 | female_last_name_pratronymic | female last name pratronymic 199 | first_name | first name 200 | full_name | full name 201 | full_name_with_prefix | full name with prefix 202 | full_name_with_suffix | full name with suffix 203 | gender | gender 204 | gender_abrev | gender abrev 205 | hex_color | hex color 206 | hex_color_short | hex color short 207 | i_pv_4 | i pv 4 208 | industry | industry 209 | job_title | job title 210 | language | language 211 | last_name | last name 212 | latitude_degrees | latitude degrees 213 | latitude_direction | latitude direction 214 | latitude_minutes | latitude minutes 215 | latitude_seconds | latitude seconds 216 | latitude | latitude 217 | longitude | longitude 218 | longitude_degrees | longitude degrees 219 | longitude_direction | longitude direction 220 | longitude_minutes | longitude minutes 221 | longitude_seconds | longitude seconds 222 | male_first_name | male first name 223 | male_full_name_with_prefix | male full name with prefix 224 | male_full_name_with_suffix | male full name with suffix 225 | male_last_name | male last name 226 | male_pratronymic | male pratronymic 227 | model | model 228 | month | month 229 | month_num | month num 230 | month_short | month short 231 | paragraph | paragraph 232 | patagraphs | patagraphs 233 | patagraphs_n | patagraphs of maximum n 234 | password | password 235 | patronymic | patronymic 236 | phone | phone 237 | product | product 238 | product_name | product name 239 | sentence | sentence 240 | sentences | sentences 241 | sentences_n | sentences of maximum n 242 | simple_pass_word | simple pass word 243 | state | state 244 | state_abbrev | state abbrev 245 | street | street 246 | street_address | street address 247 | title | title 248 | top_level_domain | top level domain 249 | user_name | user name 250 | week_day | week day 251 | week_day_short | week day short 252 | week_day_num | week day num 253 | word | word 254 | words | words 255 | words_n | words of maximum n 256 | year | year 257 | zip | zip 258 | 259 | # Benchmark 260 | This Benchmark uses [boom](https://github.com/rakyll/boom). After registering the sample json above run the following command (Note this is just to check things out, my machine is very slow) 261 | 262 | ```bash 263 | boom -n 1000 -c 100 http://localhost:3000/api/test 264 | ``` 265 | 266 | The result 267 | ```bash 268 | 269 | Summary: 270 | Total: 0.6442 secs. 271 | Slowest: 0.1451 secs. 272 | Fastest: 0.0163 secs. 273 | Average: 0.0586 secs. 274 | Requests/sec: 1552.3336 275 | Total Data Received: 39000 bytes. 276 | Response Size per Request: 39 bytes. 277 | 278 | Status code distribution: 279 | [200] 1000 responses 280 | 281 | Response time histogram: 282 | 0.016 [1] | 283 | 0.029 [121] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 284 | 0.042 [166] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 285 | 0.055 [192] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 286 | 0.068 [192] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 287 | 0.081 [168] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 288 | 0.094 [69] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 289 | 0.106 [41] |∎∎∎∎∎∎∎∎ 290 | 0.119 [22] |∎∎∎∎ 291 | 0.132 [21] |∎∎∎∎ 292 | 0.145 [7] |∎ 293 | 294 | Latency distribution: 295 | 10% in 0.0280 secs. 296 | 25% in 0.0364 secs. 297 | 50% in 0.0560 secs. 298 | 75% in 0.0751 secs. 299 | 90% in 0.0922 secs. 300 | 95% in 0.1066 secs. 301 | 99% in 0.1287 secs. 302 | ``` 303 | 304 | # Contributing 305 | 306 | Start with clicking the star button to make the author and his neighbors happy. Then fork the repository and submit a pull request for whatever change you want to be added to this project. 307 | 308 | If you have any questions, just open an issue. 309 | 310 | # Author 311 | Geofrey Ernest 312 | 313 | Twitter : [@gernesti](https://twitter.com/gernesti) 314 | 315 | 316 | # Licence 317 | 318 | This project is released under the MIT licence. See [LICENCE](LICENCE) for more details. 319 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package apidemic 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "math/rand" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/pmylund/go-cache" 13 | ) 14 | 15 | // Version is the version of apidemic. Apidemic uses semver. 16 | const Version = "0.4" 17 | 18 | var maxItemTime = cache.DefaultExpiration 19 | 20 | var store = func() *cache.Cache { 21 | c := cache.New(5*time.Minute, 30*time.Second) 22 | return c 23 | }() 24 | 25 | var allowedHttpMethods = []string{"OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD"} 26 | 27 | // API is the struct for the json object that is passed to apidemic for registration. 28 | type API struct { 29 | Endpoint string `json:"endpoint"` 30 | HTTPMethod string `json:"http_method"` 31 | ResponseCodeProbabilities map[int]int `json:"response_code_probabilities"` 32 | Payload map[string]interface{} `json:"payload"` 33 | } 34 | 35 | // Home renders hopme page. It renders a json response with information about the service. 36 | func Home(w http.ResponseWriter, r *http.Request) { 37 | details := make(map[string]interface{}) 38 | details["app_name"] = "ApiDemic" 39 | details["version"] = Version 40 | details["details"] = "Fake JSON API response" 41 | RenderJSON(w, http.StatusOK, details) 42 | return 43 | } 44 | 45 | // FindResponseCode helps imitating the backend responding with an error message occasionally 46 | // Example: 47 | // {"404": 8, "403": 12, "500": 20, "503": 3} 48 | // 8% chance of getting 404 49 | // 12% chance of getting a 500 error 50 | // 3% chance of getting a 503 error 51 | // 77% chance of getting 200 OK or 201 Created depending on the HTTP method 52 | func FindResponseCode(responseCodeProbabilities map[int]int, method string) int { 53 | sum := 0 54 | r := rand.Intn(100) 55 | 56 | for code, probability := range responseCodeProbabilities { 57 | if probability+sum > r { 58 | return code 59 | } 60 | sum = sum + probability 61 | } 62 | 63 | if method == "POST" { 64 | return http.StatusCreated 65 | } 66 | 67 | return http.StatusOK 68 | } 69 | 70 | // RenderJSON helper for rendering JSON response, it marshals value into json and writes 71 | // it into w. 72 | func RenderJSON(w http.ResponseWriter, code int, value interface{}) { 73 | if code >= 400 || code == http.StatusNoContent { 74 | http.Error(w, "", code) 75 | return 76 | } 77 | 78 | w.Header().Set("Content-Type", "application/json") 79 | w.WriteHeader(code) 80 | err := json.NewEncoder(w).Encode(value) 81 | if err != nil { 82 | http.Error(w, err.Error(), http.StatusInternalServerError) 83 | return 84 | } 85 | } 86 | 87 | // RegisterEndpoint receives API objects and registers them. The payload from the request is 88 | // transformed into a self aware Value that is capable of faking its own attribute. 89 | func RegisterEndpoint(w http.ResponseWriter, r *http.Request) { 90 | var httpMethod string 91 | a := API{} 92 | err := json.NewDecoder(r.Body).Decode(&a) 93 | if err != nil { 94 | RenderJSON(w, http.StatusBadRequest, NewResponse(err.Error())) 95 | return 96 | } 97 | 98 | if httpMethod, err = getAllowedMethod(a.HTTPMethod); err != nil { 99 | RenderJSON(w, http.StatusBadRequest, NewResponse(err.Error())) 100 | return 101 | } 102 | 103 | eKey, rcpKey := getCacheKeys(a.Endpoint, httpMethod) 104 | if _, ok := store.Get(eKey); ok { 105 | RenderJSON(w, http.StatusOK, NewResponse("endpoint already taken")) 106 | return 107 | } 108 | obj := NewObject() 109 | err = obj.Load(a.Payload) 110 | if err != nil { 111 | RenderJSON(w, http.StatusInternalServerError, NewResponse(err.Error())) 112 | return 113 | } 114 | store.Set(eKey, obj, maxItemTime) 115 | store.Set(rcpKey, a.ResponseCodeProbabilities, maxItemTime) 116 | RenderJSON(w, http.StatusOK, NewResponse("cool")) 117 | } 118 | 119 | func getCacheKeys(endpoint, httpMethod string) (string, string) { 120 | eKey := fmt.Sprintf("%s-%v-e", endpoint, httpMethod) 121 | rcpKey := fmt.Sprintf("%s-%v-rcp", endpoint, httpMethod) 122 | 123 | return eKey, rcpKey 124 | } 125 | 126 | func getAllowedMethod(method string) (string, error) { 127 | if method == "" { 128 | return "GET", nil 129 | } 130 | 131 | for _, m := range allowedHttpMethods { 132 | if method == m { 133 | return m, nil 134 | } 135 | } 136 | 137 | return "", errors.New("HTTP method is not allowed") 138 | } 139 | 140 | // DynamicEndpoint renders registered endpoints. 141 | func DynamicEndpoint(w http.ResponseWriter, r *http.Request) { 142 | vars := mux.Vars(r) 143 | 144 | eKey, rcpKey := getCacheKeys(vars["endpoint"], r.Method) 145 | if eVal, ok := store.Get(eKey); ok { 146 | if rcpVal, ok := store.Get(rcpKey); ok { 147 | code := FindResponseCode(rcpVal.(map[int]int), r.Method) 148 | RenderJSON(w, code, eVal) 149 | return 150 | } 151 | } 152 | responseText := fmt.Sprintf("apidemic: %s has no %s endpoint", vars["endpoint"], r.Method) 153 | RenderJSON(w, http.StatusNotFound, NewResponse(responseText)) 154 | } 155 | 156 | // NewResponse helper for response JSON message 157 | func NewResponse(message string) interface{} { 158 | return struct { 159 | Text string `json:"text"` 160 | }{ 161 | message, 162 | } 163 | } 164 | 165 | // NewServer returns a new apidemic server 166 | func NewServer() *mux.Router { 167 | m := mux.NewRouter() 168 | m.HandleFunc("/", Home) 169 | m.HandleFunc("/register", RegisterEndpoint).Methods("POST") 170 | m.HandleFunc("/api/{endpoint}", DynamicEndpoint).Methods("OPTIONS", "GET", "POST", "PUT", "DELETE", "HEAD") 171 | return m 172 | } 173 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package apidemic 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | "time" 12 | 13 | "github.com/gorilla/mux" 14 | "github.com/pmylund/go-cache" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestDynamicEndpointFailsWithoutRegistration(t *testing.T) { 20 | s := setUp() 21 | payload := registerPayload(t, "fixtures/sample_request.json") 22 | 23 | w := httptest.NewRecorder() 24 | req := jsonRequest("POST", "/api/test", payload) 25 | s.ServeHTTP(w, req) 26 | 27 | assert.Equal(t, http.StatusNotFound, w.Code) 28 | } 29 | 30 | func TestDynamicEndpointWithGetRequest(t *testing.T) { 31 | s := setUp() 32 | payload := registerPayload(t, "fixtures/sample_request.json") 33 | 34 | w := httptest.NewRecorder() 35 | req := jsonRequest("POST", "/register", payload) 36 | s.ServeHTTP(w, req) 37 | require.Equal(t, http.StatusOK, w.Code) 38 | 39 | w = httptest.NewRecorder() 40 | req = jsonRequest("GET", "/api/test", nil) 41 | s.ServeHTTP(w, req) 42 | 43 | assert.Equal(t, http.StatusOK, w.Code) 44 | } 45 | 46 | func TestDynamicEndpointWithPostRequest(t *testing.T) { 47 | s := setUp() 48 | payload := registerPayload(t, "fixtures/sample_request.json") 49 | payload["http_method"] = "POST" 50 | 51 | w := httptest.NewRecorder() 52 | req := jsonRequest("POST", "/register", payload) 53 | s.ServeHTTP(w, req) 54 | require.Equal(t, http.StatusOK, w.Code) 55 | 56 | w = httptest.NewRecorder() 57 | req = jsonRequest("POST", "/api/test", nil) 58 | 59 | s.ServeHTTP(w, req) 60 | assert.Equal(t, http.StatusCreated, w.Code) 61 | } 62 | 63 | func TestDynamicEndpointWithForbiddenResponse(t *testing.T) { 64 | s := setUp() 65 | registerPayload := registerPayload(t, "fixtures/sample_request.json") 66 | registerPayload["response_code_probabilities"] = map[string]int{"403": 100} 67 | 68 | w := httptest.NewRecorder() 69 | req := jsonRequest("POST", "/register", registerPayload) 70 | s.ServeHTTP(w, req) 71 | require.Equal(t, http.StatusOK, w.Code) 72 | 73 | w = httptest.NewRecorder() 74 | req = jsonRequest("GET", "/api/test", nil) 75 | 76 | s.ServeHTTP(w, req) 77 | assert.Equal(t, http.StatusForbidden, w.Code) 78 | } 79 | 80 | func setUp() *mux.Router { 81 | store = cache.New(5*time.Minute, 30*time.Second) 82 | 83 | return NewServer() 84 | } 85 | 86 | func registerPayload(t *testing.T, fixtureFile string) map[string]interface{} { 87 | content, err := ioutil.ReadFile(fixtureFile) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | var api map[string]interface{} 93 | err = json.NewDecoder(bytes.NewReader(content)).Decode(&api) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | return api 99 | } 100 | 101 | func jsonRequest(method string, path string, body interface{}) *http.Request { 102 | var bEnd io.Reader 103 | if body != nil { 104 | b, err := json.Marshal(body) 105 | if err != nil { 106 | return nil 107 | } 108 | bEnd = bytes.NewReader(b) 109 | } 110 | req, err := http.NewRequest(method, path, bEnd) 111 | if err != nil { 112 | panic(err) 113 | } 114 | req.Header.Set("Content-Type", "application/json") 115 | return req 116 | } 117 | -------------------------------------------------------------------------------- /cmd/apidemic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/codegangsta/cli" 9 | "github.com/gernest/apidemic" 10 | ) 11 | 12 | func server(ctx *cli.Context) { 13 | port := ctx.Int("port") 14 | s := apidemic.NewServer() 15 | 16 | log.Println("starting server on port :", port) 17 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), s)) 18 | } 19 | 20 | func main() { 21 | app := cli.NewApp() 22 | app.Name = "apidemic" 23 | app.Usage = "Fake JSON API Responses" 24 | app.Authors = []cli.Author{ 25 | {"Geofrey Ernest", "geofreyernest@live.com"}, 26 | } 27 | app.Version = apidemic.Version 28 | app.Commands = []cli.Command{ 29 | cli.Command{ 30 | Name: "start", 31 | ShortName: "s", 32 | Usage: "starts apidemic server", 33 | Action: server, 34 | Flags: []cli.Flag{ 35 | cli.IntFlag{ 36 | Name: "port", 37 | Usage: "HTTP port to run", 38 | Value: 3000, 39 | EnvVar: "PORT", 40 | }, 41 | }, 42 | }, 43 | } 44 | app.RunAndExitOnError() 45 | } 46 | -------------------------------------------------------------------------------- /fixtures/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anton", 3 | "age": 29, 4 | "nothing": null, 5 | "true": true, 6 | "false": false, 7 | "list": [ 8 | "first", 9 | "second" 10 | ], 11 | "list2": [ 12 | { 13 | "street": "Street 42", 14 | "city": "Stockholm" 15 | }, 16 | { 17 | "street": "Street 42", 18 | "city": "Stockholm" 19 | } 20 | ], 21 | "address": { 22 | "street": "Street 42", 23 | "city": "Stockholm" 24 | }, 25 | "country": { 26 | "name": "Sweden" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fixtures/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "name: first_name": "anton", 3 | "age: digits_n,max=2": 29, 4 | "nothing:": null, 5 | "true": true, 6 | "false": false, 7 | "list:word,max=3": [ 8 | "first", 9 | "second" 10 | ], 11 | "list2": [ 12 | { 13 | "street:street": "Street 42", 14 | "city:city": "Stockholm" 15 | }, 16 | { 17 | "street": "Street 42", 18 | "city": "Stockholm" 19 | } 20 | ], 21 | "address": { 22 | "street:street": "Street 42", 23 | "city:city": "Stockholm" 24 | }, 25 | "country": { 26 | "name:country": "Sweden" 27 | } 28 | } -------------------------------------------------------------------------------- /fixtures/sample_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoint": "test", 3 | "payload": { 4 | "name: first_name": "anton", 5 | "age: digits_n,max=2": 29, 6 | "nothing:": null, 7 | "true": true, 8 | "false": false, 9 | "list:word,max=3": [ 10 | "first", 11 | "second" 12 | ], 13 | "list2": [ 14 | { 15 | "street:street": "Street 42", 16 | "city:city": "Stockholm" 17 | }, 18 | { 19 | "street": "Street 42", 20 | "city": "Stockholm" 21 | } 22 | ], 23 | "address": { 24 | "street:street": "Street 42", 25 | "city:city": "Stockholm" 26 | }, 27 | "country": { 28 | "name:country": "Sweden" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package apidemic 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/icrowley/fake" 10 | ) 11 | 12 | type Value struct { 13 | Tags Tags 14 | Data interface{} 15 | } 16 | 17 | func (v Value) Update() Value { 18 | switch v.Data.(type) { 19 | case string: 20 | return fakeString(&v) 21 | case float64: 22 | return fakeFloats(&v) 23 | case []interface{}: 24 | return fakeArray(&v) 25 | case map[string]interface{}: 26 | return fakeObject(&v) 27 | } 28 | return v 29 | } 30 | 31 | func (v Value) MarshalJSON() ([]byte, error) { 32 | return json.Marshal(v.Update().Data) 33 | } 34 | 35 | func NewValue(val interface{}) Value { 36 | return Value{Tags: make(Tags), Data: val} 37 | } 38 | 39 | type Object struct { 40 | Data map[string]Value 41 | } 42 | 43 | func NewObject() *Object { 44 | return &Object{Data: make(map[string]Value)} 45 | } 46 | 47 | func (o *Object) Load(src map[string]interface{}) error { 48 | for key, val := range src { 49 | value := NewValue(val) 50 | sections := strings.Split(key, ":") 51 | if len(sections) == 2 { 52 | key = sections[0] 53 | value.Tags.Load(sections[1]) 54 | } 55 | o.Set(key, value) 56 | } 57 | return nil 58 | } 59 | 60 | func (o *Object) Set(key string, val Value) { 61 | o.Data[key] = val 62 | } 63 | 64 | func (v *Object) MarshalJSON() ([]byte, error) { 65 | return json.Marshal(v.Data) 66 | } 67 | 68 | func parseJSONData(src io.Reader) (*Object, error) { 69 | var in map[string]interface{} 70 | err := json.NewDecoder(src).Decode(&in) 71 | if err != nil { 72 | return nil, err 73 | } 74 | o := NewObject() 75 | err = o.Load(in) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return o, nil 80 | } 81 | 82 | func fakeString(v *Value) Value { 83 | return Value{Data: genFakeData(v)} 84 | } 85 | 86 | func fakeArray(v *Value) Value { 87 | arrV, ok := v.Data.([]interface{}) 88 | if !ok { 89 | return *v 90 | } 91 | nv := *v 92 | var rst []interface{} 93 | if len(arrV) > 0 { 94 | origin := arrV[0] 95 | n := len(arrV) 96 | 97 | if max, ok := v.Tags.Get("max"); ok { 98 | maxV, err := strconv.Atoi(max) 99 | if err != nil { 100 | return *v 101 | } 102 | n = maxV 103 | } 104 | for i := 0; i < n; i++ { 105 | newVal := NewValue(origin) 106 | rst = append(rst, newVal) 107 | } 108 | } 109 | nv.Data = rst 110 | return nv 111 | } 112 | 113 | func fakeFloats(v *Value) Value { 114 | return Value{Data: genFakeData(v)} 115 | } 116 | 117 | func fakeObject(v *Value) Value { 118 | obj := NewObject() 119 | obj.Load(v.Data.(map[string]interface{})) 120 | return NewValue(obj.Data) 121 | } 122 | 123 | func genFakeData(v *Value) interface{} { 124 | if len(v.Tags) == 0 { 125 | return v.Data 126 | } 127 | 128 | typ, ok := v.Tags.Get("type") 129 | if !ok { 130 | return v.Data 131 | } 132 | switch typ { 133 | case fieldTags.Brand: 134 | return fake.Brand() 135 | case fieldTags.Character: 136 | return fake.Character() 137 | case fieldTags.Characters: 138 | return fieldTags.Characters 139 | case fieldTags.CharactersN: 140 | max := 5 141 | if m, err := v.Tags.Int("max"); err == nil { 142 | max = m 143 | } 144 | return fake.CharactersN(max) 145 | case fieldTags.City: 146 | return fake.City() 147 | case fieldTags.Color: 148 | return fake.Color() 149 | case fieldTags.Company: 150 | return fake.Company() 151 | case fieldTags.Continent: 152 | return fake.Continent() 153 | case fieldTags.Country: 154 | return fake.Country() 155 | case fieldTags.CreditCardNum: 156 | vendor, _ := v.Tags.Get("vendor") 157 | fake.CreditCardNum(vendor) 158 | case fieldTags.Currency: 159 | fake.Currency() 160 | case fieldTags.CurrencyCode: 161 | fake.CurrencyCode() 162 | case fieldTags.Day: 163 | return fake.Day() 164 | case fieldTags.Digits: 165 | return fake.Digits() 166 | case fieldTags.DigitsN: 167 | max := 5 168 | if m, err := v.Tags.Int("max"); err == nil { 169 | max = m 170 | } 171 | return fake.DigitsN(max) 172 | case fieldTags.DomainName: 173 | return fake.DomainName() 174 | case fieldTags.DomainZone: 175 | return fake.DomainZone() 176 | case fieldTags.EmailAddress: 177 | return fake.EmailAddress() 178 | case fieldTags.EmailBody: 179 | return fake.EmailBody() 180 | case fieldTags.FemaleFirstName: 181 | return fake.FemaleFirstName() 182 | case fieldTags.FemaleFullName: 183 | return fake.FemaleFullName() 184 | case fieldTags.FemaleFullNameWithPrefix: 185 | return fake.FemaleFullNameWithPrefix() 186 | case fieldTags.FemaleFullNameWithSuffix: 187 | return fake.FemaleFullNameWithSuffix() 188 | case fieldTags.FemaleLastName: 189 | return fake.FemaleLastName() 190 | case fieldTags.FemaleLastNamePratronymic: 191 | return fake.FemalePatronymic() 192 | case fieldTags.FirstName: 193 | return fake.FirstName() 194 | case fieldTags.FullName: 195 | return fake.FullName() 196 | case fieldTags.FullNameWithPrefix: 197 | return fake.FullNameWithPrefix() 198 | case fieldTags.FullNameWithSuffix: 199 | return fake.FullNameWithSuffix() 200 | case fieldTags.Gender: 201 | return fake.Gender() 202 | case fieldTags.GenderAbrev: 203 | return fake.GenderAbbrev() 204 | case fieldTags.HexColor: 205 | return fake.HexColor() 206 | case fieldTags.HexColorShort: 207 | return fake.HexColorShort() 208 | case fieldTags.IPv4: 209 | return fake.IPv4() 210 | case fieldTags.Industry: 211 | return fake.Industry() 212 | case fieldTags.JobTitle: 213 | return fake.JobTitle() 214 | case fieldTags.Language: 215 | return fake.Language() 216 | case fieldTags.LastName: 217 | return fake.LastName() 218 | case fieldTags.LatitudeDegrees: 219 | return fake.LatitudeDegrees() 220 | case fieldTags.LatitudeDirection: 221 | return fake.LatitudeDirection() 222 | case fieldTags.LatitudeMinutes: 223 | return fake.LatitudeMinutes() 224 | case fieldTags.LatitudeSeconds: 225 | return fake.LatitudeSeconds() 226 | case fieldTags.Latitude: 227 | return fake.Latitude() 228 | case fieldTags.LongitudeDegrees: 229 | return fake.LongitudeDegrees() 230 | case fieldTags.LongitudeDirection: 231 | return fake.LongitudeDirection() 232 | case fieldTags.LongitudeMinutes: 233 | return fake.LongitudeMinutes() 234 | case fieldTags.LongitudeSeconds: 235 | return fake.LongitudeSeconds() 236 | case fieldTags.MaleFirstName: 237 | return fake.MaleFirstName() 238 | case fieldTags.MaleFullNameWithPrefix: 239 | return fake.MaleFullNameWithPrefix() 240 | case fieldTags.MaleFullNameWithSuffix: 241 | return fake.MaleFullNameWithSuffix() 242 | case fieldTags.MaleLastName: 243 | return fake.MaleLastName() 244 | case fieldTags.MalePratronymic: 245 | return fake.MalePatronymic() 246 | case fieldTags.Model: 247 | return fake.Model() 248 | case fieldTags.Month: 249 | return fake.Month() 250 | case fieldTags.MonthNum: 251 | return fake.MonthNum() 252 | case fieldTags.MonthShort: 253 | return fake.MonthShort() 254 | case fieldTags.Paragraph: 255 | return fake.Paragraph() 256 | case fieldTags.Patagraphs: 257 | return fake.Paragraphs() 258 | case fieldTags.PatagraphsN: 259 | max := 5 260 | if m, err := v.Tags.Int("max"); err == nil { 261 | max = m 262 | } 263 | return fake.ParagraphsN(max) 264 | case fieldTags.Password: 265 | var ( 266 | atLeast = 5 267 | atMost = 8 268 | allowUpper, allowNumeric, allowSpecial = true, true, true 269 | ) 270 | if least, err := v.Tags.Int("at_least"); err == nil { 271 | atLeast = least 272 | } 273 | if most, err := v.Tags.Int("at_most"); err == nil { 274 | atMost = most 275 | } 276 | if upper, err := v.Tags.Bool("upper"); err == nil { 277 | allowUpper = upper 278 | } 279 | if numeric, err := v.Tags.Bool("numeric"); err == nil { 280 | allowNumeric = numeric 281 | } 282 | if special, err := v.Tags.Bool("special"); err == nil { 283 | allowSpecial = special 284 | } 285 | return fake.Password(atLeast, atMost, allowUpper, allowNumeric, allowSpecial) 286 | case fieldTags.Patronymic: 287 | return fake.Patronymic() 288 | case fieldTags.Phone: 289 | return fake.Phone() 290 | case fieldTags.Product: 291 | return fake.Product() 292 | case fieldTags.ProductName: 293 | return fake.ProductName() 294 | case fieldTags.Sentence: 295 | return fake.Sentence() 296 | case fieldTags.Sentences: 297 | return fake.Sentence() 298 | case fieldTags.SentencesN: 299 | max := 5 300 | if m, err := v.Tags.Int("max"); err == nil { 301 | max = m 302 | } 303 | return fake.SentencesN(max) 304 | case fieldTags.SimplePassWord: 305 | return fake.SimplePassword() 306 | case fieldTags.State: 307 | return fake.State() 308 | case fieldTags.StateAbbrev: 309 | return fake.StateAbbrev() 310 | case fieldTags.Street: 311 | return fake.Street() 312 | case fieldTags.StreetAddress: 313 | return fake.StreetAddress() 314 | case fieldTags.Title: 315 | return fake.Title() 316 | case fieldTags.TopLevelDomain: 317 | return fake.TopLevelDomain() 318 | case fieldTags.UserName: 319 | return fake.UserName() 320 | case fieldTags.WeekDay: 321 | return fake.WeekDay() 322 | case fieldTags.WeekDayNum: 323 | return fake.WeekdayNum() 324 | case fieldTags.WeekDayShort: 325 | return fake.WeekDayShort() 326 | case fieldTags.Word: 327 | return fake.Word() 328 | case fieldTags.Words: 329 | return fake.Words() 330 | case fieldTags.WordsN: 331 | max := 5 332 | if m, err := v.Tags.Int("max"); err == nil { 333 | max = m 334 | } 335 | return fake.WordsN(max) 336 | case fieldTags.Year: 337 | //return fake.Year() 338 | case fieldTags.Zip: 339 | return fake.Zip() 340 | } 341 | 342 | return v.Data 343 | } 344 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package apidemic 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func TestParseJSONData(t *testing.T) { 11 | data, err := ioutil.ReadFile("fixtures/sample.json") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | ob, err := parseJSONData(bytes.NewReader(data)) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | _, err = json.MarshalIndent(ob, "", "\t") 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package apidemic 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var ErrTagNotFound = errors.New("apidemic: Tag not found") 10 | 11 | var fieldTags = struct { 12 | Brand string 13 | Character string 14 | Characters string 15 | CharactersN string 16 | City string 17 | Color string 18 | Company string 19 | Continent string 20 | Country string 21 | CreditCardNum string 22 | Currency string 23 | CurrencyCode string 24 | Day string 25 | Digits string 26 | DigitsN string 27 | DomainName string 28 | DomainZone string 29 | EmailAddress string 30 | EmailBody string 31 | FemaleFirstName string 32 | FemaleFullName string 33 | FemaleFullNameWithPrefix string 34 | FemaleFullNameWithSuffix string 35 | FemaleLastName string 36 | FemaleLastNamePratronymic string 37 | FirstName string 38 | FullName string 39 | FullNameWithPrefix string 40 | FullNameWithSuffix string 41 | Gender string 42 | GenderAbrev string 43 | HexColor string 44 | HexColorShort string 45 | IPv4 string 46 | Industry string 47 | JobTitle string 48 | Language string 49 | LastName string 50 | LatitudeDegrees string 51 | LatitudeDirection string 52 | LatitudeMinutes string 53 | LatitudeSeconds string 54 | Latitude string 55 | Longitude string 56 | LongitudeDegrees string 57 | LongitudeDirection string 58 | LongitudeMinutes string 59 | LongitudeSeconds string 60 | MaleFirstName string 61 | MaleFullNameWithPrefix string 62 | MaleFullNameWithSuffix string 63 | MaleLastName string 64 | MalePratronymic string 65 | Model string 66 | Month string 67 | MonthNum string 68 | MonthShort string 69 | Paragraph string 70 | Patagraphs string 71 | PatagraphsN string 72 | Password string 73 | Patronymic string 74 | Phone string 75 | Product string 76 | ProductName string 77 | Sentence string 78 | Sentences string 79 | SentencesN string 80 | SimplePassWord string 81 | State string 82 | StateAbbrev string 83 | Street string 84 | StreetAddress string 85 | Title string 86 | TopLevelDomain string 87 | UserName string 88 | WeekDay string 89 | WeekDayShort string 90 | WeekDayNum string 91 | Word string 92 | Words string 93 | WordsN string 94 | Year string 95 | Zip string 96 | }{ 97 | "brand", "character", "characters", "characters_n", 98 | "city", "color", "company", "continent", "country", 99 | "credit_card_num", "currency", "currency_code", "day", 100 | "digits", "digits_n", "domain_name", "domain_zone", 101 | "email_address", "email_body", "female_first_name", 102 | "female_full_name", "female_full_name_with_prefix", 103 | "female_full_name_with_suffix", "female_last_name", 104 | "female_last_name_pratronymic", "first_name", "full_name", 105 | "full_name_with_prefix", "full_name_with_suffix", "gender", 106 | "gender_abrev", "hex_color", "hex_color_short", "i_pv_4", 107 | "industry", "job_title", "language", "last_name", 108 | "latitude_degrees", "latitude_direction", "latitude_minutes", 109 | "latitude_seconds", "latitude", "longitude", "longitude_degrees", 110 | "longitude_direction", "longitude_minutes", "longitude_seconds", 111 | "male_first_name", "male_full_name_with_prefix", "male_full_name_with_suffix", 112 | "male_last_name", "male_pratronymic", "model", "month", 113 | "month_num", "month_short", "paragraph", "patagraphs", "patagraphs_n", 114 | "password", "patronymic", "phone", "product", "product_name", "sentence", 115 | "sentences", "sentences_n", "simple_pass_word", "state", "state_abbrev", 116 | "street", "street_address", "title", "top_level_domain", "user_name", "week_day", 117 | "week_day_short", "week_day_num", "word", "words", "words_n", "year", "zip", 118 | } 119 | 120 | //Tags stores metadata about values 121 | type Tags map[string]string 122 | 123 | // Load parses src and extacts tags from it. The src is a string with comma separated content. 124 | // Example "character_n,max=30" 125 | // 126 | // The first tag, is the value type information, the rest is extra information to fine tune 127 | // the generated fake value. 128 | // 129 | // For instance in the example above, the value is characters, where max=30 limits the number of characters 130 | // to the maximum size of 30. 131 | func (t Tags) Load(src string) { 132 | ss := strings.Split(src, ",") 133 | first := strings.TrimSpace(ss[0]) 134 | if len(ss) > 0 { 135 | t["type"] = first 136 | rest := ss[1:] 137 | for _, v := range rest { 138 | ts := strings.Split(v, "=") 139 | if len(ts) < 2 { 140 | t[v] = "" 141 | continue 142 | } 143 | t[strings.TrimSpace(ts[0])] = strings.TrimSpace(ts[1]) 144 | } 145 | } 146 | 147 | } 148 | 149 | // Get returns the value for tag key. 150 | func (t Tags) Get(key string) (string, bool) { 151 | k, ok := t[key] 152 | return k, ok 153 | } 154 | 155 | // Int returns an in value for tag key. 156 | func (t Tags) Int(key string) (int, error) { 157 | tag, ok := t.Get(key) 158 | if !ok { 159 | return 0, ErrTagNotFound 160 | } 161 | return strconv.Atoi(tag) 162 | } 163 | 164 | // Bool returns a boolean value for tag key 165 | func (t Tags) Bool(key string) (bool, error) { 166 | tag, ok := t.Get(key) 167 | if !ok { 168 | return false, ErrTagNotFound 169 | } 170 | return strconv.ParseBool(tag) 171 | } 172 | -------------------------------------------------------------------------------- /tags_test.go: -------------------------------------------------------------------------------- 1 | package apidemic 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTags(t *testing.T) { 8 | src := "characters_n,max=30" 9 | sample := []struct { 10 | tag, value string 11 | }{ 12 | {"type", "characters_n"}, 13 | {"max", "30"}, 14 | } 15 | 16 | tags := make(Tags) 17 | tags.Load(src) 18 | for _, v := range sample { 19 | k, ok := tags.Get(v.tag) 20 | if !ok { 21 | t.Errorf("expected %s to exist %#v", v.tag, tags) 22 | } 23 | if k != v.value { 24 | t.Errorf("expected %s got %s", v.value, k) 25 | } 26 | } 27 | 28 | max, err := tags.Int("max") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if max != 30 { 33 | t.Errorf("expected %d got %d", 30, max) 34 | } 35 | } 36 | --------------------------------------------------------------------------------