├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── app.go ├── config ├── config.go ├── config_test.go └── test.yml ├── endpoints ├── endpoint.go └── endpoint_test.go ├── generator └── generator.go ├── sample ├── responses │ ├── groups_get.json │ ├── groups_post.json │ ├── users_get.json │ ├── users_post.json │ └── users_put.json └── sample.yml └── stubble-gopher.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # VSCode specific 27 | .vscode/* 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: go 3 | go: 4 | - 1.6 5 | - tip 6 | install: 7 | - make install-deps 8 | script: make test 9 | matrix: 10 | allow_failures: 11 | - go: tip 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Eric Irwin 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | cd endpoints && go test 3 | cd config && go test 4 | 5 | install-deps: 6 | go get github.com/jwaldrip/odin/cli 7 | go get gopkg.in/yaml.v2 8 | 9 | install: 10 | go install 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #stubble - Mock JSON API Generator [![GitHub release](https://img.shields.io/github/release/eirwin/stubble.svg)](https://github.com/eirwin/stubble/releases) [![Build Status](https://travis-ci.org/EIrwin/stubble.svg?branch=master)](https://travis-ci.org/eirwin/stubble) 2 | 3 | 4 | 5 | ## What is stubble? 6 | Stubble is a mock JSON API generator that uses a YAML specification to define mock API endpoints and responses. 7 | 8 | ## Why stubble? 9 | Current API response mocking solutions bloat client and/or server side code. Stubble can be ran 100% from your client and server leaving it clean and free of unecessary bloat. 10 | 11 | ## Example 12 | 13 | Stubble expects a simple `YAML` configuration to generate a mock JSON API. 14 | 15 | ```yaml 16 | host: "localhost" 17 | 18 | port: "8282" 19 | 20 | endpoints: 21 | - "GET /api/v1/users responses/users_get.json" 22 | - "POST /api/v1/users responses/users_post.json" 23 | - "PUT /api/v1/users responses/users_put.json" 24 | - "GET /api/v1/groups responses/groups_get.json" 25 | - "POST /api/v1/groups responses/groups_post.json" 26 | ``` 27 | 28 | Assuming binary is available, running the following will result in a stubble server being generated. The command assumes we have a file named `sample.yaml` that contains the configuration above. 29 | 30 | `./stubble -p=sample.yml` 31 | 32 | This results in the following `stubble.go` file to be generated. 33 | 34 | ```go 35 | package main 36 | 37 | import ( 38 | "io/ioutil" 39 | "log" 40 | "net/http" 41 | ) 42 | 43 | func main() { 44 | log.Println("Running stubble server on localhost:8282") 45 | 46 | http.HandleFunc("/api/v1/groups", func(w http.ResponseWriter, r *http.Request) { 47 | 48 | if "GET" == r.Method { 49 | file, _ := ioutil.ReadFile("responses/groups_get.json") 50 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 51 | w.Write(file) 52 | } 53 | 54 | if "POST" == r.Method { 55 | file, _ := ioutil.ReadFile("responses/groups_post.json") 56 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 57 | w.Write(file) 58 | } 59 | 60 | }) 61 | 62 | http.HandleFunc("/api/v1/users", func(w http.ResponseWriter, r *http.Request) { 63 | 64 | if "GET" == r.Method { 65 | file, _ := ioutil.ReadFile("responses/users_get.json") 66 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 67 | w.Write(file) 68 | } 69 | 70 | if "POST" == r.Method { 71 | file, _ := ioutil.ReadFile("responses/users_post.json") 72 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 73 | w.Write(file) 74 | } 75 | 76 | if "PUT" == r.Method { 77 | file, _ := ioutil.ReadFile("responses/users_put.json") 78 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 79 | w.Write(file) 80 | } 81 | 82 | }) 83 | 84 | log.Fatal(http.ListenAndServe("localhost:8282", nil)) 85 | } 86 | ``` 87 | 88 | Finally, we can start our stubble server by simply running the command 89 | 90 | `go run stubble.go` 91 | 92 | ## Samples 93 | To run the samples in `/samples`, perform the following steps 94 | 95 | Try it out (Currently only can be done manually) 96 | 97 | 1.Clone the repository 98 | ``` 99 | git clone git@github.com:eirwin/stubble 100 | ``` 101 | 2.Build and/or install 102 | ``` 103 | go build 104 | go install 105 | ``` 106 | 3.Navigate to `sample` directory 107 | ``` 108 | cd sample 109 | ``` 110 | 4.Run stubble generator against YAML configuration 111 | ``` 112 | $GOPATH/bin/stubble -p=sample.yml 113 | ``` 114 | 5.Start stubble server 115 | ``` 116 | go run stubble.go 117 | ``` 118 | 6.Test out stubble 119 | ``` 120 | curl localhost:8282/api/v1/users 121 | ``` 122 | 123 | If everything was done correctly, you should see the following 124 | 125 | ``` 126 | curl localhost:8282/api/v1/users 127 | { 128 | "users" : [ 129 | { 130 | "name" : "User A" 131 | }, 132 | { 133 | "group": "User B" 134 | }, 135 | { 136 | "group": "User C" 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | 143 | ## Development 144 | 145 | Stubble uses go templates to build http handlers in Go. 146 | 147 | ## Contributing 148 | 149 | 1. Fork it ( https://github.com/eirwin/stubble/fork ) 150 | 2. Create your feature branch (git checkout -b my-new-feature) 151 | 3. Commit your changes (git commit -am 'Add some feature') 152 | 4. Push to the branch (git push origin my-new-feature) 153 | 5. Create a new Pull Request 154 | 155 | ## Tasks 156 | 1. Add Makefile 157 | 2. Add additional content-type support. 158 | 3. Implement HTTP response code support. 159 | 4. Dynamic data in responses using templating. 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/jwaldrip/odin/cli" 7 | 8 | "github.com/eirwin/stubby/config" 9 | "github.com/eirwin/stubby/generator" 10 | ) 11 | 12 | const ( 13 | version = "0.0.1" 14 | title = "Stubble - Mock JSON API Generator" 15 | pathFlag = "path" 16 | ) 17 | 18 | var app = cli.New(version, title, func(c cli.Command) { 19 | if c.Flag(pathFlag) != nil { 20 | path := c.Flag("path").Get().(string) 21 | config, err := config.Read(path) 22 | if err != nil { 23 | log.Println(err.Error()) 24 | } 25 | 26 | generator := generator.New(config) 27 | err = generator.Run() 28 | if err != nil { 29 | log.Println(err.Error()) 30 | } 31 | } 32 | }) 33 | 34 | func init() { 35 | app.DefineStringFlag("path", "", "Path to .yaml file defining Stubble configuration") 36 | app.AliasFlag('p', "path") 37 | } 38 | 39 | func main() { 40 | app.Start() 41 | } 42 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type Config struct { 10 | Host string 11 | Port string 12 | Endpoints []string 13 | } 14 | 15 | func Read(path string) (Config, error) { 16 | var config Config 17 | source, err := ioutil.ReadFile(path) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | err = yaml.Unmarshal(source, &config) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return config, nil 27 | } 28 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | testConfigPath = "test.yml" 10 | testEndpoint = "GET /test test_response.json" 11 | testHost = "localhost" 12 | testPort = "8282" 13 | ) 14 | 15 | func TestRead(t *testing.T) { 16 | c, err := Read(testConfigPath) 17 | if err != nil { 18 | t.Errorf(err.Error()) 19 | } 20 | 21 | if c.Host != testHost { 22 | log.Println("'invalid host read from config yml") 23 | t.Fail() 24 | } 25 | 26 | if c.Port != testPort { 27 | log.Println("invalid port read from config yml") 28 | t.Fail() 29 | } 30 | 31 | if c.Endpoints[0] != testEndpoint { 32 | log.Println("invalid endpoint read from config yml") 33 | t.Fail() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/test.yml: -------------------------------------------------------------------------------- 1 | host: "localhost" 2 | 3 | port: "8282" 4 | 5 | endpoints: 6 | - "GET /test test_response.json" -------------------------------------------------------------------------------- /endpoints/endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type Endpoint struct { 9 | Method string 10 | Path string 11 | FilePath string 12 | Code int 13 | } 14 | 15 | func Parse(definition string) (Endpoint, error) { 16 | var endpoint Endpoint 17 | parts := strings.Split(definition, " ") 18 | parts = removeEmptyParts(parts) 19 | length := len(parts) 20 | 21 | //parse Method 22 | if length >= 1 { 23 | endpoint.Method = parts[0] 24 | } 25 | 26 | //parse Path 27 | if length >= 2 { 28 | endpoint.Path = parts[1] 29 | } 30 | 31 | //parse file Path 32 | if length >= 3 { 33 | endpoint.FilePath = parts[2] 34 | } 35 | 36 | //parse code 37 | if length >= 4 { 38 | endpoint.Code, _ = strconv.Atoi(parts[3]) 39 | } 40 | return endpoint, nil 41 | } 42 | 43 | func removeEmptyParts(p []string) []string { 44 | var parts []string 45 | for _,part := range p { 46 | if part != "" { 47 | parts = append(parts,part) 48 | } 49 | } 50 | return parts 51 | } 52 | -------------------------------------------------------------------------------- /endpoints/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | testDefinition = "GET /test test_response.json" 10 | testDefinitionMultipleSpaces = "GET /test test_response.json" 11 | testMethod = "GET" 12 | testPath = "/test" 13 | testFilePath = "test_response.json" 14 | ) 15 | 16 | func TestParse(t *testing.T) { 17 | assertParseResult(testDefinition,t) 18 | } 19 | 20 | func TestParseWithMultipleSpaces(t *testing.T){ 21 | assertParseResult(testDefinitionMultipleSpaces,t) 22 | } 23 | 24 | func assertParseResult(definition string,t *testing.T) { 25 | e, err := Parse(definition) 26 | if err != nil { 27 | t.Error(err.Error()) 28 | } 29 | 30 | if e.Method != testMethod { 31 | log.Println("'invalid parsed method") 32 | t.Fail() 33 | } 34 | 35 | if e.Path != testPath { 36 | log.Println("'invalid parsed path") 37 | t.Fail() 38 | } 39 | 40 | if e.FilePath != testFilePath { 41 | log.Println("invalid parsed file path") 42 | t.Fail() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "os" 7 | 8 | "github.com/eirwin/stubby/config" 9 | "github.com/eirwin/stubby/endpoints" 10 | ) 11 | 12 | const ( 13 | templateName = "Stubble" 14 | ) 15 | 16 | type Generator struct { 17 | Config config.Config 18 | } 19 | 20 | type generatorContext struct { 21 | Host string 22 | Port string 23 | EndpointMap map[string][]endpoints.Endpoint 24 | } 25 | 26 | func (g Generator) Run() error { 27 | endpoints, err := parseEndpoints(g.Config) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | endpointMap := generateEndpointMap(endpoints) 33 | 34 | context := generatorContext{ 35 | Host: g.Config.Host, 36 | Port: g.Config.Port, 37 | EndpointMap: endpointMap, 38 | } 39 | 40 | t := template.New(templateName) 41 | t, err = t.Parse(generator()) 42 | if err != nil { 43 | panic(err) 44 | } 45 | if err == nil { 46 | buff := bytes.NewBufferString("") 47 | t.Execute(buff, context) 48 | writeFile(buff) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func parseEndpoints(c config.Config) ([]endpoints.Endpoint, error) { 55 | var parsed []endpoints.Endpoint 56 | for _, endpoint := range c.Endpoints { 57 | e, err := endpoints.Parse(endpoint) 58 | if err != nil { 59 | return parsed, err 60 | } 61 | parsed = append(parsed, e) 62 | } 63 | return parsed, nil 64 | } 65 | 66 | func generateEndpointMap(definitions []endpoints.Endpoint) map[string][]endpoints.Endpoint { 67 | endpointMap := make(map[string][]endpoints.Endpoint, 0) 68 | for _, def := range definitions { 69 | _, ok := endpointMap[def.Path] 70 | var defs []endpoints.Endpoint 71 | if ok { 72 | defs = endpointMap[def.Path] 73 | } else { 74 | defs = make([]endpoints.Endpoint, 0) 75 | } 76 | defs = append(defs, def) 77 | endpointMap[def.Path] = defs 78 | } 79 | return endpointMap 80 | } 81 | 82 | func writeFile(buffer *bytes.Buffer) error { 83 | f, err := os.Create("stubble.go") 84 | if err != nil { 85 | return err 86 | } 87 | _, err = f.Write(buffer.Bytes()) 88 | if err != nil { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | func New(c config.Config) Generator { 95 | return Generator{ 96 | Config: c, 97 | } 98 | 99 | } 100 | 101 | func generator() string { 102 | return ` 103 | package main 104 | 105 | import ( 106 | "net/http" 107 | "io/ioutil" 108 | "log" 109 | ) 110 | 111 | func main(){ 112 | log.Println("Running stubble server on {{.Host}}:{{.Port}}") 113 | {{with .EndpointMap}} 114 | {{ range $key, $value := . }} 115 | {{with $value}} 116 | http.HandleFunc("{{ $key }}", func(w http.ResponseWriter, r *http.Request) { 117 | {{ range .}} 118 | if "{{ .Method }}" == r.Method { 119 | file,_ := ioutil.ReadFile("{{.FilePath}}") 120 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 121 | w.Write(file) 122 | } 123 | {{end}} 124 | }) 125 | {{end}} 126 | {{end}} 127 | {{end}} 128 | log.Fatal(http.ListenAndServe("{{.Host}}:{{.Port}}", nil)) 129 | }` 130 | } 131 | -------------------------------------------------------------------------------- /sample/responses/groups_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups" : [ 3 | { 4 | "name" : "Group A" 5 | }, 6 | { 7 | "group": "Group B" 8 | }, 9 | { 10 | "group": "Group C" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /sample/responses/groups_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "method" : "POST", 3 | "success" : true 4 | } -------------------------------------------------------------------------------- /sample/responses/users_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "users" : [ 3 | { 4 | "name" : "User A" 5 | }, 6 | { 7 | "group": "User B" 8 | }, 9 | { 10 | "group": "User C" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /sample/responses/users_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "method" : "POST", 3 | "success" : true 4 | } -------------------------------------------------------------------------------- /sample/responses/users_put.json: -------------------------------------------------------------------------------- 1 | { 2 | "method" : "PUT", 3 | "success" : true 4 | } -------------------------------------------------------------------------------- /sample/sample.yml: -------------------------------------------------------------------------------- 1 | host: "localhost" 2 | 3 | port: "8282" 4 | 5 | endpoints: 6 | - "GET /api/v1/users responses/users_get.json" 7 | - "POST /api/v1/users responses/users_post.json" 8 | - "PUT /api/v1/users responses/users_put.json" 9 | - "GET /api/v1/groups responses/groups_get.json" 10 | - "POST /api/v1/groups responses/groups_post.json" -------------------------------------------------------------------------------- /stubble-gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EIrwin/stubble/07826330acc30604bb34d8663ed6e4c04064947a/stubble-gopher.png --------------------------------------------------------------------------------