├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── apisprout.go ├── apisprout_test.go ├── example.go ├── example_test.go ├── go.mod ├── go.sum ├── helm ├── Chart.yaml ├── README.md ├── templates │ ├── deployment.yaml │ └── service.yaml └── values.yaml ├── logo.gvdesign ├── release.sh └── testdata └── example ├── recursive_cycles.yml ├── recursive_infinite.yml ├── recursive_ok.yml └── recursive_seen_twice.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor 3 | logo.png 4 | apisprout 5 | apisprout.exe 6 | apisprout*.zip 7 | apisprout*.xz 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: go 3 | go: 4 | - stable 5 | env: 6 | - GO111MODULE=on 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | - Return header examples when possible. 9 | - Update dependency versions. 10 | 11 | ## [1.3.0] - 2019-03-18 12 | - Add `--add-server` to add a custom server when using `--validate-server`. 13 | This allows quickly adding a custom domain or base path that will properly 14 | validate. 15 | - Add `--header` (short `-H`) option to specify a custom header when fetching 16 | the API document. This allows you to pass custom auth info. 17 | - Add `readOnly` and `writeOnly` support to the example generator. 18 | - Revamped support for `--validate-server` (short `-s`) 19 | - Requires the use of server base path(s) on the client. 20 | - Localhost is now always allowed on all known base paths. 21 | - Support for proxy headers (e.g. `X-Forwarded-Host`). 22 | - Better support for resolving relative path references. 23 | - Be more resilient to parser panics when using `--watch` 24 | - Update Docker build to use Go 1.12 and Go modules. 25 | - Enhance example-from-schema generation code. Support enums, string formats, 26 | array and object examples, min/max and min items. 27 | 28 | ## [1.2.0] - 2019-02-27 29 | - Add support for reloading OpenAPI URLs via `/__reload` on the server. 30 | - Support external references in OpenAPI loader. 31 | - Update dependencies, simplify file loading. 32 | - Support jsonapi.org content type (`application/vnd.api+json`). 33 | - Switch from `dep` to Go modules. 34 | 35 | ## [1.1.1] - 2019-01-30 36 | - Fix `OPTIONS` request to also include CORS headers. 37 | 38 | ## [1.1.0] - 2019-01-29 39 | - Added the `--watch` (short `-w`) parameter to reload whenever the input file 40 | changes. This currently only works when using files on disk. 41 | - Update Docker build to use Go 1.11. 42 | - Generate examples from schema when no example is available. 43 | - Fix path parameter validation. 44 | - Add CORS headers. Disable with `--disable-cors`. 45 | - Documentation updates. 46 | 47 | ## [1.0.1] - 2018-10-03 48 | - Dependency updates, fixes string format validation bug. 49 | 50 | ## [1.0.0] - 2018-07-24 51 | - Initial release. 52 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12-alpine as build 2 | WORKDIR /apisprout 3 | COPY . . 4 | RUN apk add --no-cache git && \ 5 | go get github.com/ahmetb/govvv && \ 6 | govvv install 7 | 8 | FROM alpine:3.8 9 | COPY --from=build /go/bin/apisprout /usr/local/bin/ 10 | RUN apk add --no-cache ca-certificates && \ 11 | update-ca-certificates 12 | ENTRYPOINT ["/usr/local/bin/apisprout"] 13 | EXPOSE 8000 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2019 Daniel G. Taylor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | API Sprout 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/danielgtaylor/apisprout)](https://goreportcard.com/report/github.com/danielgtaylor/apisprout) [![Build Status](https://travis-ci.org/danielgtaylor/apisprout.svg?branch=master)](https://travis-ci.org/danielgtaylor/apisprout) [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/danielgtaylor/apisprout.svg)](https://github.com/danielgtaylor/apisprout/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/danielgtaylor/apisprout.svg)](https://hub.docker.com/r/danielgtaylor/apisprout/) 4 | 5 | A simple, quick, cross-platform API mock server that returns examples specified in an API description document. Features include: 6 | 7 | - OpenAPI 3.x support 8 | - Uses operation `examples` or generates examples from `schema` 9 | - Load from a URL or local file (auto reload with `--watch`) 10 | - CORS headers enabled by default 11 | - Accept header content negotiation 12 | - Example: `Accept: application/*` 13 | - Prefer header to select response to test specific cases 14 | - Example: `Prefer: status=409` 15 | - Server validation (enabled with `--validate-server`) 16 | - Validates scheme, hostname/port, and base path 17 | - Supports `localhost` out of the box 18 | - Use the `--add-server` flag, in conjunction with `--validate-server`, to dynamically include more servers in the validation logic 19 | - Request parameter & body validation (enabled with `--validate-request`) 20 | - Configuration via: 21 | - Files (`/etc/apisprout/config.json|yaml`) 22 | - Environment (prefixed with `SPROUT_`, e.g. `SPROUT_VALIDATE_SERVER`) 23 | - Commandline flags 24 | 25 | Usage is simple: 26 | 27 | ```sh 28 | # Load from a local file 29 | apisprout my-api.yaml 30 | 31 | # Validate server name and use base path 32 | apisprout --validate-server my-api.yaml 33 | 34 | # Dynamically Include a new server / path in the validation 35 | apisprout --add-server http://localhost:8080/mock --validate-server my-api.yaml 36 | 37 | # Load from a URL 38 | apisprout https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/api-with-examples.yaml 39 | ``` 40 | 41 | ## Docker Image 42 | 43 | A hosted [API Sprout Docker image](https://hub.docker.com/r/danielgtaylor/apisprout/) is provided that makes it easy to deploy mock servers or run locally. For example: 44 | 45 | ```sh 46 | docker pull danielgtaylor/apisprout 47 | docker run -p 8000:8000 danielgtaylor/apisprout http://example.com/my-api.yaml 48 | ``` 49 | 50 | Configuration can be passed via environment variables, e.g. setting `SPROUT_VALIDATE_REQUEST=1`, or by passing commandline flags. It is also possible to use a local API description file via [Docker Volumes](https://docs.docker.com/storage/volumes/): 51 | 52 | ``` 53 | # Remember to put the full path to local archive YAML in -v 54 | docker run -p 8000:8000 -v $FULLPATH/localfile.yaml:/api.yaml danielgtaylor/apisprout /api.yaml 55 | ``` 56 | 57 | ## Installation 58 | 59 | Download the appropriate binary from the [releases](https://github.com/danielgtaylor/apisprout/releases) page. 60 | 61 | Alternatively, you can use `go get`: 62 | 63 | ```sh 64 | go get github.com/danielgtaylor/apisprout 65 | ``` 66 | 67 | ## Extra Features 68 | 69 | ### Remote Reload 70 | 71 | If your API spec is loaded from a remote URL, you can live-reload it by hitting the `/__reload` endpoint. 72 | 73 | ### Health Check 74 | 75 | A simple endpoint which returns status code `200` is available at `/__health`. This endpoint successfully returns `200` even if `--validate-server` is turned on, and the endpoint is being accessed from a non-validated host. 76 | 77 | ## Contributing 78 | 79 | Contributions are very welcome. Please open a tracking issue or pull request and we can work to get things merged in. 80 | 81 | ## Release Process 82 | 83 | The following describes the steps to make a new release of API Sprout. 84 | 85 | 1. Merge open PRs you want to release. 86 | 1. Select a new semver version number (major/minor/patch depending on changes). 87 | 1. Update `CHANGELOG.md` to describe changes. 88 | 1. Create a commit for the release. 89 | 1. Tag the commit with `git tag -a -m 'Tagging x.y.z release' vx.y.z`. 90 | 1. Build release binaries with `./release.sh`. 91 | 1. Push the commit and tags. 92 | 1. Upload the release binaries. 93 | 94 | ## License 95 | 96 | Copyright © 2018-2019 Daniel G. Taylor 97 | 98 | http://dgt.mit-license.org/ 99 | -------------------------------------------------------------------------------- /apisprout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "math/rand" 10 | "mime" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | "regexp" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "github.com/fsnotify/fsnotify" 21 | "github.com/getkin/kin-openapi/openapi3" 22 | "github.com/getkin/kin-openapi/openapi3filter" 23 | "github.com/gobwas/glob" 24 | "github.com/pkg/errors" 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/pflag" 27 | "github.com/spf13/viper" 28 | yaml "gopkg.in/yaml.v2" 29 | ) 30 | 31 | // GitSummary is filled in by `govvv` for version info. 32 | var GitSummary string 33 | 34 | var ( 35 | // ErrNoExample is sent when no example was found for an operation. 36 | ErrNoExample = errors.New("No example found") 37 | 38 | // ErrRecursive is when a schema is impossible to represent because it infinitely recurses. 39 | ErrRecursive = errors.New("Recursive schema") 40 | 41 | // ErrCannotMarshal is set when an example cannot be marshalled. 42 | ErrCannotMarshal = errors.New("Cannot marshal example") 43 | 44 | // ErrMissingAuth is set when no authorization header or key is present but 45 | // one is required by the API description. 46 | ErrMissingAuth = errors.New("Missing auth") 47 | 48 | // ErrInvalidAuth is set when the authorization scheme doesn't correspond 49 | // to the one required by the API description. 50 | ErrInvalidAuth = errors.New("Invalid auth") 51 | ) 52 | 53 | var ( 54 | marshalJSONMatcher = regexp.MustCompile(`^application/(vnd\..+\+)?json$`) 55 | marshalYAMLMatcher = regexp.MustCompile(`^(application|text)/(x-|vnd\..+\+)?yaml$`) 56 | ) 57 | 58 | type RefreshableRouter struct { 59 | router *openapi3filter.Router 60 | } 61 | 62 | func (rr *RefreshableRouter) Set(router *openapi3filter.Router) { 63 | rr.router = router 64 | } 65 | 66 | func (rr *RefreshableRouter) Get() *openapi3filter.Router { 67 | return rr.router 68 | } 69 | 70 | func NewRefreshableRouter() *RefreshableRouter { 71 | return &RefreshableRouter{} 72 | } 73 | 74 | // ContentNegotiator is used to match a media type during content negotiation 75 | // of HTTP requests. 76 | type ContentNegotiator struct { 77 | globs []glob.Glob 78 | } 79 | 80 | // NewContentNegotiator creates a new negotiator from an HTTP Accept header. 81 | func NewContentNegotiator(accept string) *ContentNegotiator { 82 | // The HTTP Accept header is parsed and converted to simple globs, which 83 | // can be used to match an incoming mimetype. Example: 84 | // Accept: text/html, text/*;q=0.9, */*;q=0.8 85 | // Will be turned into the following globs: 86 | // - text/html 87 | // - text/* 88 | // - */* 89 | globs := make([]glob.Glob, 0) 90 | for _, mt := range strings.Split(accept, ",") { 91 | parsed, _, _ := mime.ParseMediaType(mt) 92 | globs = append(globs, glob.MustCompile(parsed)) 93 | } 94 | 95 | return &ContentNegotiator{ 96 | globs: globs, 97 | } 98 | } 99 | 100 | // Match returns true if the given mediatype string matches any of the allowed 101 | // types in the accept header. 102 | func (cn *ContentNegotiator) Match(mediatype string) bool { 103 | for _, glob := range cn.globs { 104 | if glob.Match(mediatype) { 105 | return true 106 | } 107 | } 108 | 109 | return false 110 | } 111 | 112 | func main() { 113 | rand.Seed(time.Now().UnixNano()) 114 | 115 | // Load configuration from file(s) if provided. 116 | viper.SetConfigName("config") 117 | viper.AddConfigPath("/etc/apisprout/") 118 | viper.AddConfigPath("$HOME/.apisprout/") 119 | viper.ReadInConfig() 120 | 121 | // Load configuration from the environment if provided. Flags below get 122 | // transformed automatically, e.g. `foo-bar` -> `SPROUT_FOO_BAR`. 123 | viper.SetEnvPrefix("SPROUT") 124 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 125 | viper.AutomaticEnv() 126 | 127 | // Build the root command. This is the application's entry point. 128 | cmd := filepath.Base(os.Args[0]) 129 | root := &cobra.Command{ 130 | Use: fmt.Sprintf("%s [flags] FILE", cmd), 131 | Version: GitSummary, 132 | Args: cobra.MinimumNArgs(1), 133 | Run: server, 134 | Example: fmt.Sprintf(" # Basic usage\n %s openapi.yaml\n\n # Validate server name and use base path\n %s --validate-server openapi.yaml\n\n # Fetch API via HTTP with custom auth header\n %s -H 'Authorization: abc123' http://example.com/openapi.yaml", cmd, cmd, cmd), 135 | } 136 | 137 | // Set up global options. 138 | flags := root.PersistentFlags() 139 | 140 | addParameter(flags, "port", "p", 8000, "HTTP port") 141 | addParameter(flags, "validate-server", "s", false, "Check scheme/hostname/basepath against configured servers") 142 | addParameter(flags, "validate-request", "", false, "Check request data structure") 143 | addParameter(flags, "watch", "w", false, "Reload when input file changes") 144 | addParameter(flags, "disable-cors", "", false, "Disable CORS headers") 145 | addParameter(flags, "header", "H", "", "Add a custom header when fetching API") 146 | addParameter(flags, "add-server", "", "", "Add a new valid server URL, use with --validate-server") 147 | addParameter(flags, "https", "", false, "Use HTTPS instead of HTTP") 148 | addParameter(flags, "public-key", "", "", "Public key for HTTPS, use with --https") 149 | addParameter(flags, "private-key", "", "", "Private key for HTTPS, use with --https") 150 | 151 | // Run the app! 152 | root.Execute() 153 | } 154 | 155 | // addParameter adds a new global parameter with a default value that can be 156 | // configured using configuration files, the environment, or commandline flags. 157 | func addParameter(flags *pflag.FlagSet, name, short string, def interface{}, desc string) { 158 | viper.SetDefault(name, def) 159 | switch v := def.(type) { 160 | case bool: 161 | flags.BoolP(name, short, v, desc) 162 | case int: 163 | flags.IntP(name, short, v, desc) 164 | case string: 165 | flags.StringP(name, short, v, desc) 166 | } 167 | viper.BindPFlag(name, flags.Lookup(name)) 168 | } 169 | 170 | // getTypedExample will return an example from a given media type, if such an 171 | // example exists. If multiple examples are given, then one is selected at 172 | // random unless an "example" item exists in the Prefer header 173 | func getTypedExample(mt *openapi3.MediaType, prefer map[string]string) (interface{}, error) { 174 | if mt.Example != nil { 175 | return mt.Example, nil 176 | } 177 | 178 | if len(mt.Examples) > 0 { 179 | // If preferred example requested and it it exists, return it 180 | preferredExample := "" 181 | if mapContainsKey(prefer, "example") { 182 | preferredExample = prefer["example"] 183 | if _, ok := mt.Examples[preferredExample]; ok { 184 | return mt.Examples[preferredExample].Value.Value, nil 185 | } 186 | } 187 | 188 | // Choose a random example to return. 189 | keys := make([]string, 0, len(mt.Examples)) 190 | for k := range mt.Examples { 191 | keys = append(keys, k) 192 | } 193 | 194 | if len(keys) > 0 { 195 | selected := keys[rand.Intn(len(keys))] 196 | return mt.Examples[selected].Value.Value, nil 197 | } 198 | } 199 | 200 | if mt.Schema != nil { 201 | return OpenAPIExample(ModeResponse, mt.Schema.Value) 202 | } 203 | // TODO: generate data from JSON schema, if no examples available? 204 | 205 | return nil, ErrNoExample 206 | } 207 | 208 | // getExample tries to return an example for a given operation. 209 | // Using the Prefer http header, the consumer can specify the type of response they want. 210 | func getExample(negotiator *ContentNegotiator, prefer map[string]string, op *openapi3.Operation) (int, string, map[string]*openapi3.HeaderRef, interface{}, error) { 211 | var responses []string 212 | var blankHeaders = make(map[string]*openapi3.HeaderRef) 213 | 214 | if !mapContainsKey(prefer, "status") { 215 | // First, make a list of responses ordered by successful (200-299 status code) 216 | // before other types. 217 | success := make([]string, 0) 218 | other := make([]string, 0) 219 | for s := range op.Responses { 220 | if status, err := strconv.Atoi(s); err == nil && status >= 200 && status < 300 { 221 | success = append(success, s) 222 | continue 223 | } 224 | other = append(other, s) 225 | } 226 | responses = append(success, other...) 227 | } else if op.Responses[prefer["status"]] != nil { 228 | responses = []string{prefer["status"]} 229 | } else if op.Responses["default"] != nil { 230 | responses = []string{"default"} 231 | } else { 232 | return 0, "", blankHeaders, nil, ErrNoExample 233 | } 234 | 235 | // Now try to find the first example we can and return it! 236 | for _, s := range responses { 237 | response := op.Responses[s] 238 | status, err := strconv.Atoi(s) 239 | if err != nil { 240 | // If we are using the default with prefer, we can use its status 241 | // code: 242 | status, err = strconv.Atoi(prefer["status"]) 243 | } 244 | if err != nil { 245 | // Otherwise, treat default and other named statuses as 200. 246 | status = http.StatusOK 247 | } 248 | 249 | if response.Value.Content == nil { 250 | // This is a valid response but has no body defined. 251 | return status, "", blankHeaders, "", nil 252 | } 253 | 254 | for mt, content := range response.Value.Content { 255 | if negotiator != nil && !negotiator.Match(mt) { 256 | // This is not what the client asked for. 257 | continue 258 | } 259 | 260 | example, err := getTypedExample(content, prefer) 261 | if err == nil { 262 | return status, mt, response.Value.Headers, example, nil 263 | } 264 | 265 | fmt.Printf("Error getting example: %v\n", err) 266 | } 267 | } 268 | 269 | return 0, "", blankHeaders, nil, ErrNoExample 270 | } 271 | 272 | // addLocalServers will ensure that requests to localhost are always allowed 273 | // even if not specified in the OpenAPI document. 274 | func addLocalServers(swagger *openapi3.Swagger) error { 275 | seen := make(map[string]bool) 276 | for _, s := range swagger.Servers { 277 | seen[s.URL] = true 278 | } 279 | 280 | lservers := make([]*openapi3.Server, 0, len(swagger.Servers)) 281 | for _, s := range swagger.Servers { 282 | u, err := url.Parse(s.URL) 283 | if err != nil { 284 | return err 285 | } 286 | 287 | if u.Hostname() != "localhost" { 288 | u.Scheme = "http" 289 | u.Host = fmt.Sprintf("localhost:%d", viper.GetInt("port")) 290 | 291 | ls := &openapi3.Server{ 292 | URL: u.String(), 293 | Description: s.Description, 294 | Variables: s.Variables, 295 | } 296 | 297 | if !seen[ls.URL] { 298 | lservers = append(lservers, ls) 299 | seen[ls.URL] = true 300 | } 301 | } 302 | } 303 | 304 | if len(lservers) != 0 { 305 | swagger.Servers = append(swagger.Servers, lservers...) 306 | } 307 | 308 | return nil 309 | } 310 | 311 | // Load the OpenAPI document and create the router. 312 | func load(uri string, data []byte) (swagger *openapi3.Swagger, router *openapi3filter.Router, err error) { 313 | defer func() { 314 | if r := recover(); r != nil { 315 | swagger = nil 316 | router = nil 317 | if e, ok := r.(error); ok { 318 | err = errors.Wrap(e, "Caught panic while trying to load") 319 | } else { 320 | err = fmt.Errorf("Caught panic while trying to load") 321 | } 322 | } 323 | }() 324 | 325 | loader := openapi3.NewSwaggerLoader() 326 | loader.IsExternalRefsAllowed = true 327 | 328 | var u *url.URL 329 | u, err = url.Parse(uri) 330 | if err != nil { 331 | return 332 | } 333 | 334 | swagger, err = loader.LoadSwaggerFromDataWithPath(data, u) 335 | if err != nil { 336 | return 337 | } 338 | 339 | if !viper.GetBool("validate-server") { 340 | // Clear the server list so no validation happens. Note: this has a side 341 | // effect of no longer parsing any server-declared parameters. 342 | swagger.Servers = make([]*openapi3.Server, 0) 343 | } else { 344 | // Special-case localhost to always be allowed for local testing. 345 | if err = addLocalServers(swagger); err != nil { 346 | return 347 | } 348 | 349 | if cs := viper.GetString("add-server"); cs != "" { 350 | swagger.Servers = append(swagger.Servers, &openapi3.Server{ 351 | URL: cs, 352 | Description: "Custom server from command line param", 353 | Variables: make(map[string]*openapi3.ServerVariable), 354 | }) 355 | } 356 | } 357 | 358 | // Create a new router using the OpenAPI document's declared paths. 359 | router = openapi3filter.NewRouter().WithSwagger(swagger) 360 | 361 | return 362 | } 363 | 364 | // parsePreferHeader takes the value of a prefer header and splits it out into key value pairs 365 | // 366 | // HTTP Prefer header specification examples: 367 | // - Prefer: status=200; example="something" 368 | // - Prefer: example=something;status=200; 369 | // - Prefer: example="somet,;hing";status=200; 370 | // 371 | // As part of the Prefer specification, it is completely valid to specify 372 | // multiple Prefer headers in a single request, however we won't be 373 | // supporting that for the moment and only the first Prefer header 374 | // will be used. 375 | func parsePreferHeader(value string) map[string]string { 376 | prefer := map[string]string{} 377 | if value != "" { 378 | // In the event that something is quoted, we want to pull those items out of the string 379 | // and save them for later, so they don't conflict with other splitting logic. 380 | 381 | quotedRegex := regexp.MustCompile(`"[^"]*"`) 382 | splitRegex := regexp.MustCompile(`(,|;| )`) 383 | wilcardRegex := regexp.MustCompile(`%%([0-9]+)%%`) 384 | 385 | quotedStrings := quotedRegex.FindAllString(value, -1) 386 | if len(quotedStrings) > 0 { 387 | // replace each quoted string with a placehoder 388 | for idx, quotedString := range quotedStrings { 389 | value = strings.Replace(value, quotedString, fmt.Sprintf("%%%%%v%%%%", idx), 1) 390 | } 391 | } 392 | 393 | pairs := splitRegex.Split(value, -1) 394 | for _, pair := range pairs { 395 | pair = strings.TrimSpace(pair) 396 | if pair != "" { 397 | // Put any wildcards back 398 | wildcardStrings := wilcardRegex.FindAllStringSubmatch(pair, -1) 399 | for _, wildcard := range wildcardStrings { 400 | quotedIdx, _ := strconv.Atoi(wildcard[1]) 401 | pair = strings.Replace(pair, wildcard[0], quotedStrings[quotedIdx], 1) 402 | } 403 | 404 | // Determine the key and valid for this argument 405 | if strings.Contains(pair, "=") { 406 | eqIdx := strings.Index(pair, "=") 407 | prefer[pair[:eqIdx]] = strings.Trim(pair[eqIdx+1:], `"`) 408 | } else { 409 | prefer[pair] = "" 410 | } 411 | } 412 | } 413 | } 414 | return prefer 415 | } 416 | 417 | func mapContainsKey(dict map[string]string, key string) bool { 418 | if _, ok := dict[key]; ok { 419 | return true 420 | } 421 | return false 422 | } 423 | 424 | var handler = func(rr *RefreshableRouter) http.Handler { 425 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 426 | if !viper.GetBool("disable-cors") { 427 | corsOrigin := req.Header.Get("Origin") 428 | if corsOrigin == "" { 429 | corsOrigin = "*" 430 | } 431 | w.Header().Set("Access-Control-Allow-Origin", corsOrigin) 432 | 433 | if corsOrigin != "*" { 434 | // Allow credentials to be sent if an origin has been specified. 435 | // This is done *outside* of an OPTIONS request since it might be 436 | // required for a non-preflighted GET/POST request. 437 | w.Header().Set("Access-Control-Allow-Credentials", "true") 438 | } 439 | 440 | // Handle pre-flight OPTIONS request 441 | if (*req).Method == "OPTIONS" { 442 | corsMethod := req.Header.Get("Access-Control-Request-Method") 443 | if corsMethod == "" { 444 | corsMethod = "POST, GET, OPTIONS, PUT, DELETE" 445 | } 446 | 447 | corsHeaders := req.Header.Get("Access-Control-Request-Headers") 448 | if corsHeaders == "" { 449 | corsHeaders = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization" 450 | } 451 | 452 | w.Header().Set("Access-Control-Allow-Methods", corsMethod) 453 | w.Header().Set("Access-Control-Allow-Headers", corsHeaders) 454 | return 455 | } 456 | } 457 | 458 | info := fmt.Sprintf("%s %v", req.Method, req.URL) 459 | 460 | // Set up the request, handling potential proxy headers 461 | req.URL.Host = req.Host 462 | fHost := req.Header.Get("X-Forwarded-Host") 463 | if fHost != "" { 464 | req.URL.Host = fHost 465 | } 466 | 467 | req.URL.Scheme = "http" 468 | if req.Header.Get("X-Forwarded-Proto") == "https" || 469 | req.Header.Get("X-Forwarded-Scheme") == "https" || 470 | strings.Contains(req.Header.Get("Forwarded"), "proto=https") { 471 | req.URL.Scheme = "https" 472 | } 473 | 474 | if viper.GetBool("validate-server") { 475 | // Use the scheme/host in the log message since we are validating it. 476 | info = fmt.Sprintf("%s %v", req.Method, req.URL) 477 | } 478 | 479 | route, pathParams, err := rr.Get().FindRoute(req.Method, req.URL) 480 | if err != nil { 481 | log.Printf("ERROR: %s => %v", info, err) 482 | w.WriteHeader(http.StatusNotFound) 483 | return 484 | } 485 | 486 | if viper.GetBool("validate-request") { 487 | err = openapi3filter.ValidateRequest(nil, &openapi3filter.RequestValidationInput{ 488 | Request: req, 489 | Route: route, 490 | PathParams: pathParams, 491 | Options: &openapi3filter.Options{ 492 | AuthenticationFunc: func(c context.Context, input *openapi3filter.AuthenticationInput) error { 493 | // TODO: support more schemes 494 | sec := input.SecurityScheme 495 | if sec.Type == "http" { 496 | // Prefixes for each scheme. 497 | prefixes := map[string]string{ 498 | "bearer": "BEARER ", 499 | "basic": "BASIC ", 500 | } 501 | if prefix, ok := prefixes[sec.Scheme]; ok { 502 | auth := req.Header.Get("Authorization") 503 | // If the auth is missing 504 | if len(auth) == 0 { 505 | return ErrMissingAuth 506 | } 507 | // If the auth doesn't have a value or doesn't start with the case insensitive prefix 508 | if len(auth) <= len(prefix) || !strings.HasPrefix(strings.ToUpper(auth), prefix) { 509 | return ErrInvalidAuth 510 | } 511 | } 512 | } 513 | return nil 514 | }, 515 | }, 516 | }) 517 | if err != nil { 518 | log.Printf("ERROR: %s => %v", info, err) 519 | w.WriteHeader(http.StatusBadRequest) 520 | w.Write([]byte(fmt.Sprintf("%v", err))) 521 | return 522 | } 523 | } 524 | 525 | var negotiator *ContentNegotiator 526 | if accept := req.Header.Get("Accept"); accept != "" { 527 | negotiator = NewContentNegotiator(accept) 528 | if accept != "*/*" { 529 | info = fmt.Sprintf("%s (Accept %s)", info, accept) 530 | } 531 | } 532 | 533 | prefer := parsePreferHeader(req.Header.Get("Prefer")) 534 | 535 | status, mediatype, headers, example, err := getExample(negotiator, prefer, route.Operation) 536 | if err != nil { 537 | log.Printf("%s => Missing example", info) 538 | w.WriteHeader(http.StatusTeapot) 539 | w.Write([]byte("No example available.")) 540 | return 541 | } 542 | 543 | id := route.Operation.OperationID 544 | if id == "" { 545 | id = route.Operation.Summary 546 | } 547 | 548 | log.Printf("%s (%s) => %d (%s)", info, id, status, mediatype) 549 | 550 | var encoded []byte 551 | 552 | if s, ok := example.(string); ok { 553 | encoded = []byte(s) 554 | } else if _, ok := example.([]byte); ok { 555 | encoded = example.([]byte) 556 | } else { 557 | if marshalJSONMatcher.MatchString(mediatype) { 558 | encoded, err = json.MarshalIndent(example, "", " ") 559 | } else if marshalYAMLMatcher.MatchString(mediatype) { 560 | encoded, err = yaml.Marshal(example) 561 | } else { 562 | log.Printf("Cannot marshal as '%s'!", mediatype) 563 | err = ErrCannotMarshal 564 | } 565 | 566 | if err != nil { 567 | w.WriteHeader(http.StatusInternalServerError) 568 | w.Write([]byte("Unable to marshal response")) 569 | return 570 | } 571 | } 572 | 573 | for name, header := range headers { 574 | if header.Value != nil { 575 | example := name 576 | 577 | if header.Value.Schema != nil && header.Value.Schema.Value != nil { 578 | if v, err := OpenAPIExample(ModeResponse, header.Value.Schema.Value); err == nil { 579 | if vs, ok := v.(string); ok { 580 | example = vs 581 | } else { 582 | fmt.Printf("Could not convert example value '%v' to string", v) 583 | } 584 | } 585 | } 586 | 587 | w.Header().Set(name, example) 588 | } 589 | } 590 | 591 | if mediatype != "" { 592 | w.Header().Set("Content-Type", mediatype) 593 | } 594 | 595 | w.WriteHeader(status) 596 | w.Write(encoded) 597 | }) 598 | } 599 | 600 | // server loads an OpenAPI file and runs a mock server using the paths and 601 | // examples defined in the file. 602 | func server(cmd *cobra.Command, args []string) { 603 | var swagger *openapi3.Swagger 604 | rr := NewRefreshableRouter() 605 | 606 | uri := args[0] 607 | 608 | var err error 609 | var data []byte 610 | dataType := strings.Trim(strings.ToLower(filepath.Ext(uri)), ".") 611 | 612 | // Load either from an HTTP URL or from a local file depending on the passed 613 | // in value. 614 | if strings.HasPrefix(uri, "http") { 615 | req, err := http.NewRequest("GET", uri, nil) 616 | if err != nil { 617 | log.Fatal(err) 618 | } 619 | if customHeader := viper.GetString("header"); customHeader != "" { 620 | header := strings.Split(customHeader, ":") 621 | if len(header) != 2 { 622 | log.Fatal("Header format is invalid.") 623 | } 624 | req.Header.Add(strings.TrimSpace(header[0]), strings.TrimSpace(header[1])) 625 | } 626 | client := &http.Client{} 627 | resp, err := client.Do(req) 628 | if err != nil { 629 | log.Fatal(err) 630 | } 631 | 632 | data, err = ioutil.ReadAll(resp.Body) 633 | resp.Body.Close() 634 | if err != nil { 635 | log.Fatal(err) 636 | } 637 | 638 | if viper.GetBool("watch") { 639 | log.Fatal("Watching a URL is not supported.") 640 | } 641 | } else { 642 | data, err = ioutil.ReadFile(uri) 643 | if err != nil { 644 | log.Fatal(err) 645 | } 646 | 647 | if viper.GetBool("watch") { 648 | // Set up a new filesystem watcher and reload the router every time 649 | // the file has changed on disk. 650 | watcher, err := fsnotify.NewWatcher() 651 | if err != nil { 652 | log.Fatal(err) 653 | } 654 | defer watcher.Close() 655 | 656 | go func() { 657 | // Since waiting for events or errors is blocking, we do this in a 658 | // goroutine. It loops forever here but will exit when the process 659 | // is finished, e.g. when you `ctrl+c` to exit. 660 | for { 661 | select { 662 | case event, ok := <-watcher.Events: 663 | if !ok { 664 | return 665 | } 666 | if event.Op&fsnotify.Write == fsnotify.Write { 667 | fmt.Printf("🌙 Reloading %s\n", uri) 668 | data, err = ioutil.ReadFile(uri) 669 | if err != nil { 670 | log.Fatal(err) 671 | } 672 | 673 | if s, r, err := load(uri, data); err == nil { 674 | swagger = s 675 | rr.Set(r) 676 | } else { 677 | log.Printf("ERROR: Unable to load OpenAPI document: %s", err) 678 | } 679 | } 680 | case err, ok := <-watcher.Errors: 681 | if !ok { 682 | return 683 | } 684 | fmt.Println("error:", err) 685 | } 686 | } 687 | }() 688 | 689 | watcher.Add(uri) 690 | } 691 | } 692 | 693 | swagger, router, err := load(uri, data) 694 | if err != nil { 695 | log.Fatal(err) 696 | } 697 | 698 | rr.Set(router) 699 | 700 | if strings.HasPrefix(uri, "http") { 701 | http.HandleFunc("/__reload", func(w http.ResponseWriter, r *http.Request) { 702 | resp, err := http.Get(uri) 703 | if err != nil { 704 | log.Printf("ERROR: %v", err) 705 | w.WriteHeader(http.StatusBadRequest) 706 | w.Write([]byte("error while reloading")) 707 | return 708 | } 709 | 710 | data, err = ioutil.ReadAll(resp.Body) 711 | resp.Body.Close() 712 | if err != nil { 713 | log.Printf("ERROR: %v", err) 714 | w.WriteHeader(http.StatusBadRequest) 715 | w.Write([]byte("error while parsing")) 716 | return 717 | } 718 | 719 | if s, r, err := load(uri, data); err == nil { 720 | swagger = s 721 | rr.Set(r) 722 | } 723 | 724 | w.WriteHeader(200) 725 | w.Write([]byte("reloaded")) 726 | log.Printf("Reloaded from %s", uri) 727 | }) 728 | } 729 | 730 | // Add a health check route which returns 200 731 | http.HandleFunc("/__health", func(w http.ResponseWriter, r *http.Request) { 732 | w.WriteHeader(200) 733 | log.Printf("Health check") 734 | }) 735 | 736 | // Another custom handler to return the exact swagger document given to us 737 | http.HandleFunc("/__schema", func(w http.ResponseWriter, req *http.Request) { 738 | if !viper.GetBool("disable-cors") { 739 | corsOrigin := req.Header.Get("Origin") 740 | if corsOrigin == "" { 741 | corsOrigin = "*" 742 | } 743 | w.Header().Set("Access-Control-Allow-Origin", corsOrigin) 744 | } 745 | 746 | w.Header().Set("Content-Type", fmt.Sprintf("application/%v; charset=utf-8", dataType)) 747 | w.WriteHeader(http.StatusOK) 748 | fmt.Fprint(w, string(data)) 749 | }) 750 | 751 | // Register our custom HTTP handler that will use the router to find 752 | // the appropriate OpenAPI operation and try to return an example. 753 | http.Handle("/", handler(rr)) 754 | 755 | format := "🌱 Sprouting %s on port %d" 756 | if viper.GetBool("https") { 757 | format = "🌱 Securely sprouting %s on port %d" 758 | } 759 | fmt.Printf(format, swagger.Info.Title, viper.GetInt("port")) 760 | 761 | if viper.GetBool("validate-server") && len(swagger.Servers) != 0 { 762 | fmt.Printf(" with valid servers:\n") 763 | for _, s := range swagger.Servers { 764 | fmt.Println("• " + s.URL) 765 | } 766 | } else { 767 | fmt.Printf("\n") 768 | } 769 | 770 | port := fmt.Sprintf(":%d", viper.GetInt("port")) 771 | if viper.GetBool("https") { 772 | err = http.ListenAndServeTLS(port, viper.GetString("public-key"), 773 | viper.GetString("private-key"), nil) 774 | } else { 775 | err = http.ListenAndServe(port, nil) 776 | } 777 | if err != nil { 778 | log.Fatal(err) 779 | } 780 | } 781 | -------------------------------------------------------------------------------- /apisprout_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/getkin/kin-openapi/openapi3" 11 | "github.com/spf13/viper" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | var localServerTests = []struct { 17 | name string 18 | in []string 19 | out []string 20 | }{ 21 | { 22 | "No servers", 23 | []string{}, 24 | []string{}, 25 | }, 26 | { 27 | "Same path", 28 | []string{ 29 | "https://api.example.com/v1", 30 | "https://beta.api.example.com/v1", 31 | }, 32 | []string{ 33 | "https://api.example.com/v1", 34 | "https://beta.api.example.com/v1", 35 | "http://localhost:8000/v1", 36 | }, 37 | }, 38 | { 39 | "Includes localhost already", 40 | []string{ 41 | "https://api.example.com/v1", 42 | "http://localhost:8000/v1", 43 | }, 44 | []string{ 45 | "https://api.example.com/v1", 46 | "http://localhost:8000/v1", 47 | }, 48 | }, 49 | { 50 | "Invalid URL", 51 | []string{ 52 | "http://192.168.0.%31/", 53 | }, 54 | []string{}, 55 | }, 56 | } 57 | 58 | func TestAddLocalServers(t *testing.T) { 59 | viper.SetDefault("port", 8000) 60 | for _, tt := range localServerTests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | servers := make([]*openapi3.Server, len(tt.in)) 63 | for i, u := range tt.in { 64 | servers[i] = &openapi3.Server{ 65 | URL: u, 66 | } 67 | } 68 | 69 | s := &openapi3.Swagger{ 70 | Servers: servers, 71 | } 72 | 73 | err := addLocalServers(s) 74 | if len(tt.in) > 0 && len(tt.out) == 0 { 75 | assert.Error(t, err) 76 | return 77 | } 78 | assert.NoError(t, err) 79 | 80 | results := make([]string, 0, len(tt.out)) 81 | for _, server := range s.Servers { 82 | results = append(results, server.URL) 83 | } 84 | 85 | assert.Equal(t, tt.out, results) 86 | }) 87 | } 88 | } 89 | 90 | func TestParsePreferHeader(t *testing.T) { 91 | tests := []struct { 92 | name string 93 | header string 94 | want map[string]string 95 | }{ 96 | { 97 | name: "Single", 98 | header: "status=200", 99 | want: map[string]string{ 100 | "status": "200", 101 | }, 102 | }, 103 | { 104 | name: "Single Quotes", 105 | header: "status=\"200\"", 106 | want: map[string]string{ 107 | "status": "200", 108 | }, 109 | }, 110 | { 111 | name: "Single Quotes Space", 112 | header: "example=\"in progress\"", 113 | want: map[string]string{ 114 | "example": "in progress", 115 | }, 116 | }, 117 | { 118 | name: "Multiple Semicolon", 119 | header: "status=200;example=complete", 120 | want: map[string]string{ 121 | "status": "200", 122 | "example": "complete", 123 | }, 124 | }, 125 | { 126 | name: "Multiple Semi Space", 127 | header: "status=200; example=complete", 128 | want: map[string]string{ 129 | "status": "200", 130 | "example": "complete", 131 | }, 132 | }, 133 | { 134 | name: "Multiple Comma", 135 | header: "status=200,example=complete", 136 | want: map[string]string{ 137 | "status": "200", 138 | "example": "complete", 139 | }, 140 | }, 141 | { 142 | name: "Multiple Comma Space", 143 | header: "status=200, example=complete", 144 | want: map[string]string{ 145 | "status": "200", 146 | "example": "complete", 147 | }, 148 | }, 149 | { 150 | name: "Mixed Pairs", 151 | header: "example=complete; foo, status=\"200\",", 152 | want: map[string]string{ 153 | "example": "complete", 154 | "foo": "", 155 | "status": "200", 156 | }, 157 | }, 158 | } 159 | for _, tt := range tests { 160 | t.Run(tt.name, func(t *testing.T) { 161 | if got := parsePreferHeader(tt.header); !reflect.DeepEqual(got, tt.want) { 162 | t.Errorf("parsePreferHeader() = %v, want %v", got, tt.want) 163 | } 164 | }) 165 | } 166 | } 167 | 168 | func TestMediaTypes(t *testing.T) { 169 | const schema = `{ 170 | "paths": { 171 | "/test": { 172 | "get": { 173 | "summary": "Test", 174 | "responses": { 175 | "200": { 176 | "content": { 177 | "%s": { 178 | "schema": { 179 | type": "boolean", 180 | "example": true 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | }` 190 | 191 | tests := []struct { 192 | MediaType string 193 | StatusCode int 194 | }{ 195 | { 196 | MediaType: "application/json", 197 | StatusCode: http.StatusOK, 198 | }, 199 | { 200 | MediaType: "application/vnd.test-api+json", 201 | StatusCode: http.StatusOK, 202 | }, 203 | { 204 | MediaType: "application/yaml", 205 | StatusCode: http.StatusOK, 206 | }, 207 | { 208 | MediaType: "application/x-yaml", 209 | StatusCode: http.StatusOK, 210 | }, 211 | { 212 | MediaType: "application/vnd.test-api+yaml", 213 | StatusCode: http.StatusOK, 214 | }, 215 | { 216 | MediaType: "text/yaml", 217 | StatusCode: http.StatusOK, 218 | }, 219 | { 220 | MediaType: "text/x-yaml", 221 | StatusCode: http.StatusOK, 222 | }, 223 | { 224 | MediaType: "text/vnd.test-api+yaml", 225 | StatusCode: http.StatusOK, 226 | }, 227 | { 228 | MediaType: "text/vnd.test-api+xml", 229 | StatusCode: http.StatusInternalServerError, 230 | }, 231 | { 232 | MediaType: "application/json-with-extensions", 233 | StatusCode: http.StatusInternalServerError, 234 | }, 235 | } 236 | for _, test := range tests { 237 | t.Run(test.MediaType, func(t *testing.T) { 238 | _, router, err := load("file:///swagger.json", []byte(fmt.Sprintf(schema, test.MediaType))) 239 | require.NoError(t, err) 240 | require.NotNil(t, router) 241 | 242 | rr := NewRefreshableRouter() 243 | rr.Set(router) 244 | 245 | req, err := http.NewRequest("GET", "/test", nil) 246 | require.NoError(t, err) 247 | 248 | resp := httptest.NewRecorder() 249 | handler(rr).ServeHTTP(resp, req) 250 | 251 | assert.Equal(t, test.StatusCode, resp.Code) 252 | }) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | ) 8 | 9 | // Mode defines a mode of operation for example generation. 10 | type Mode int 11 | 12 | const ( 13 | // ModeRequest is for the request body (writes to the server) 14 | ModeRequest Mode = iota 15 | // ModeResponse is for the response body (reads from the server) 16 | ModeResponse 17 | ) 18 | 19 | func getSchemaExample(schema *openapi3.Schema) (interface{}, bool) { 20 | if schema.Example != nil { 21 | return schema.Example, true 22 | } 23 | 24 | if schema.Default != nil { 25 | return schema.Default, true 26 | } 27 | 28 | if schema.Enum != nil && len(schema.Enum) > 0 { 29 | return schema.Enum[0], true 30 | } 31 | 32 | return nil, false 33 | } 34 | 35 | // stringFormatExample returns an example string based on the given format. 36 | // http://json-schema.org/latest/json-schema-validation.html#rfc.section.7.3 37 | func stringFormatExample(format string) string { 38 | switch format { 39 | case "date": 40 | // https://tools.ietf.org/html/rfc3339 41 | return "2018-07-23" 42 | case "date-time": 43 | // This is the date/time of API Sprout's first commit! :-) 44 | return "2018-07-23T22:58:00-07:00" 45 | case "time": 46 | return "22:58:00-07:00" 47 | case "email": 48 | return "email@example.com" 49 | case "hostname": 50 | // https://tools.ietf.org/html/rfc2606#page-2 51 | return "example.com" 52 | case "ipv4": 53 | // https://tools.ietf.org/html/rfc5737 54 | return "198.51.100.0" 55 | case "ipv6": 56 | // https://tools.ietf.org/html/rfc3849 57 | return "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 58 | case "uri": 59 | return "https://tools.ietf.org/html/rfc3986" 60 | case "uri-template": 61 | // https://tools.ietf.org/html/rfc6570 62 | return "http://example.com/dictionary/{term:1}/{term}" 63 | case "json-pointer": 64 | // https://tools.ietf.org/html/rfc6901 65 | return "#/components/parameters/term" 66 | case "regex": 67 | // https://stackoverflow.com/q/3296050/164268 68 | return "/^1?$|^(11+?)\\1+$/" 69 | case "uuid": 70 | // https://www.ietf.org/rfc/rfc4122.txt 71 | return "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" 72 | case "password": 73 | return "********" 74 | } 75 | 76 | return "" 77 | } 78 | 79 | // excludeFromMode will exclude a schema if the mode is request and the schema 80 | // is read-only, or if the mode is response and the schema is write only. 81 | func excludeFromMode(mode Mode, schema *openapi3.Schema) bool { 82 | if schema == nil { 83 | return true 84 | } 85 | 86 | if mode == ModeRequest && schema.ReadOnly { 87 | return true 88 | } else if mode == ModeResponse && schema.WriteOnly { 89 | return true 90 | } 91 | 92 | return false 93 | } 94 | 95 | // isRequired checks whether a key is actually required. 96 | func isRequired(schema *openapi3.Schema, key string) bool { 97 | for _, req := range schema.Required { 98 | if req == key { 99 | return true 100 | } 101 | } 102 | 103 | return false 104 | } 105 | 106 | type cachedSchema struct { 107 | pending bool 108 | out interface{} 109 | } 110 | 111 | func openAPIExample(mode Mode, schema *openapi3.Schema, cache map[*openapi3.Schema]*cachedSchema) (out interface{}, err error) { 112 | if ex, ok := getSchemaExample(schema); ok { 113 | return ex, nil 114 | } 115 | 116 | cached, ok := cache[schema] 117 | if !ok { 118 | cached = &cachedSchema{ 119 | pending: true, 120 | } 121 | cache[schema] = cached 122 | } else if cached.pending { 123 | return nil, ErrRecursive 124 | } else { 125 | return cached.out, nil 126 | } 127 | 128 | defer func() { 129 | cached.pending = false 130 | cached.out = out 131 | }() 132 | 133 | // Handle combining keywords 134 | if len(schema.OneOf) > 0 { 135 | var ex interface{} 136 | var err error 137 | 138 | for _, candidate := range schema.OneOf { 139 | ex, err = openAPIExample(mode, candidate.Value, cache) 140 | if err == nil { 141 | break 142 | } 143 | } 144 | 145 | return ex, err 146 | } 147 | if len(schema.AnyOf) > 0 { 148 | var ex interface{} 149 | var err error 150 | 151 | for _, candidate := range schema.AnyOf { 152 | ex, err = openAPIExample(mode, candidate.Value, cache) 153 | if err == nil { 154 | break 155 | } 156 | } 157 | 158 | return ex, err 159 | } 160 | if len(schema.AllOf) > 0 { 161 | example := map[string]interface{}{} 162 | 163 | for _, allOf := range schema.AllOf { 164 | candidate, err := openAPIExample(mode, allOf.Value, cache) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | value, ok := candidate.(map[string]interface{}) 170 | if !ok { 171 | return nil, ErrNoExample 172 | } 173 | 174 | for k, v := range value { 175 | example[k] = v 176 | } 177 | } 178 | 179 | return example, nil 180 | } 181 | 182 | switch { 183 | case schema.Type == "boolean": 184 | return true, nil 185 | case schema.Type == "number", schema.Type == "integer": 186 | value := 0.0 187 | 188 | if schema.Min != nil && *schema.Min > value { 189 | value = *schema.Min 190 | if schema.ExclusiveMin { 191 | if schema.Max != nil { 192 | // Make the value half way. 193 | value = (*schema.Min + *schema.Max) / 2.0 194 | } else { 195 | value++ 196 | } 197 | } 198 | } 199 | 200 | if schema.Max != nil && *schema.Max < value { 201 | value = *schema.Max 202 | if schema.ExclusiveMax { 203 | if schema.Min != nil { 204 | // Make the value half way. 205 | value = (*schema.Min + *schema.Max) / 2.0 206 | } else { 207 | value-- 208 | } 209 | } 210 | } 211 | 212 | if schema.MultipleOf != nil && int(value)%int(*schema.MultipleOf) != 0 { 213 | value += float64(int(*schema.MultipleOf) - (int(value) % int(*schema.MultipleOf))) 214 | } 215 | 216 | if schema.Type == "integer" { 217 | return int(value), nil 218 | } 219 | 220 | return value, nil 221 | case schema.Type == "string": 222 | if ex := stringFormatExample(schema.Format); ex != "" { 223 | return ex, nil 224 | } 225 | 226 | example := "string" 227 | 228 | for schema.MinLength > uint64(len(example)) { 229 | example += example 230 | } 231 | 232 | if schema.MaxLength != nil && *schema.MaxLength < uint64(len(example)) { 233 | example = example[:*schema.MaxLength] 234 | } 235 | 236 | return example, nil 237 | case schema.Type == "array", schema.Items != nil: 238 | example := []interface{}{} 239 | 240 | if schema.Items != nil && schema.Items.Value != nil { 241 | ex, err := openAPIExample(mode, schema.Items.Value, cache) 242 | if err != nil { 243 | return nil, fmt.Errorf("can't get example for array item: %+v", err) 244 | } 245 | 246 | example = append(example, ex) 247 | 248 | for uint64(len(example)) < schema.MinItems { 249 | example = append(example, ex) 250 | } 251 | } 252 | 253 | return example, nil 254 | case schema.Type == "object", len(schema.Properties) > 0: 255 | example := map[string]interface{}{} 256 | 257 | for k, v := range schema.Properties { 258 | if excludeFromMode(mode, v.Value) { 259 | continue 260 | } 261 | 262 | ex, err := openAPIExample(mode, v.Value, cache) 263 | if err == ErrRecursive { 264 | if isRequired(schema, k) { 265 | return nil, fmt.Errorf("can't get example for '%s': %+v", k, err) 266 | } 267 | } else if err != nil { 268 | return nil, fmt.Errorf("can't get example for '%s': %+v", k, err) 269 | } else { 270 | example[k] = ex 271 | } 272 | } 273 | 274 | if schema.AdditionalProperties != nil && schema.AdditionalProperties.Value != nil { 275 | addl := schema.AdditionalProperties.Value 276 | 277 | if !excludeFromMode(mode, addl) { 278 | ex, err := openAPIExample(mode, addl, cache) 279 | if err == ErrRecursive { 280 | // We just won't add this if it's recursive. 281 | } else if err != nil { 282 | return nil, fmt.Errorf("can't get example for additional properties: %+v", err) 283 | } else { 284 | example["additionalPropertyName"] = ex 285 | } 286 | } 287 | } 288 | 289 | return example, nil 290 | } 291 | 292 | return nil, ErrNoExample 293 | } 294 | 295 | // OpenAPIExample creates an example structure from an OpenAPI 3 schema 296 | // object, which is an extended subset of JSON Schema. 297 | // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject 298 | func OpenAPIExample(mode Mode, schema *openapi3.Schema) (interface{}, error) { 299 | return openAPIExample(mode, schema, make(map[*openapi3.Schema]*cachedSchema)) 300 | } 301 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/getkin/kin-openapi/openapi3" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func exampleFixture(t *testing.T, name string) string { 17 | f, err := os.Open(path.Join("testdata/example", name)) 18 | require.NoError(t, err) 19 | defer f.Close() 20 | 21 | b, err := ioutil.ReadAll(f) 22 | require.NoError(t, err) 23 | 24 | return string(b) 25 | } 26 | 27 | var schemaTests = []struct { 28 | name string 29 | in string 30 | out string 31 | }{ 32 | // ----- Booleans ----- 33 | { 34 | "Boolean", 35 | `{"type": "boolean"}`, 36 | `true`, 37 | }, 38 | { 39 | "Boolean default", 40 | `{"type": "boolean", "default": false}`, 41 | `false`, 42 | }, 43 | { 44 | "Boolean example", 45 | `{"type": "boolean", "example": false}`, 46 | `false`, 47 | }, 48 | // ----- Numbers ----- 49 | { 50 | "Integer", 51 | `{"type": "integer"}`, 52 | `0`, 53 | }, 54 | { 55 | "Number", 56 | `{"type": "number"}`, 57 | `0.0`, 58 | }, 59 | { 60 | "Number default", 61 | `{"type": "number", "default": 1.0}`, 62 | `1.0`, 63 | }, 64 | { 65 | "Number example", 66 | `{"type": "number", "example": 2.0}`, 67 | `2.0`, 68 | }, 69 | { 70 | "Number enum", 71 | `{"type": "number", "enum": [2.0, 4.0, 6.0]}`, 72 | `2.0`, 73 | }, 74 | { 75 | "Number minimum", 76 | `{"type": "number", "minimum": 5.0}`, 77 | `5.0`, 78 | }, 79 | { 80 | "Number exclusive minimum", 81 | `{"type": "number", "minimum": 5.0, "exclusiveMinimum": true}`, 82 | `6.0`, 83 | }, 84 | { 85 | "Number maximum", 86 | `{"type": "number", "maximum": -1.0}`, 87 | `-1.0`, 88 | }, 89 | { 90 | "Number exclusive maximum", 91 | `{"type": "number", "maximum": -1.0, "exclusiveMaximum": true}`, 92 | `-2.0`, 93 | }, 94 | { 95 | "Number exclusinve minimum with max", 96 | `{"type": "number", "minimum": 5.0, "exclusiveMinimum": true, "maximum": 5.5}`, 97 | `5.25`, 98 | }, 99 | { 100 | "Number exclusinve maximum with min", 101 | `{"type": "number", "minimum": -5.0, "maximum": -1.0, "exclusiveMaximum": true}`, 102 | `-3.0`, 103 | }, 104 | { 105 | "Integer multiple of", 106 | `{"type": "integer", "minimum": 1, "multipleOf": 4}`, 107 | `4`, 108 | }, 109 | // ----- Strings ----- 110 | { 111 | "String", 112 | `{"type": "string"}`, 113 | `"string"`, 114 | }, 115 | { 116 | "String default", 117 | `{"type": "string", "default": "def"}`, 118 | `"def"`, 119 | }, 120 | { 121 | "String example", 122 | `{"type": "string", "example": "ex"}`, 123 | `"ex"`, 124 | }, 125 | { 126 | "String enum", 127 | `{"type": "string", "enum": ["one", "two", "three"]}`, 128 | `"one"`, 129 | }, 130 | { 131 | "String format date", 132 | `{"type": "string", "format": "date"}`, 133 | `"2018-07-23"`, 134 | }, 135 | { 136 | "String format date-time", 137 | `{"type": "string", "format": "date-time"}`, 138 | `"2018-07-23T22:58:00-07:00"`, 139 | }, 140 | { 141 | "String format time", 142 | `{"type": "string", "format": "time"}`, 143 | `"22:58:00-07:00"`, 144 | }, 145 | { 146 | "String format email", 147 | `{"type": "string", "format": "email"}`, 148 | `"email@example.com"`, 149 | }, 150 | { 151 | "String format hostname", 152 | `{"type": "string", "format": "hostname"}`, 153 | `"example.com"`, 154 | }, 155 | { 156 | "String format ipv4", 157 | `{"type": "string", "format": "ipv4"}`, 158 | `"198.51.100.0"`, 159 | }, 160 | { 161 | "String format ipv6", 162 | `{"type": "string", "format": "ipv6"}`, 163 | `"2001:0db8:85a3:0000:0000:8a2e:0370:7334"`, 164 | }, 165 | { 166 | "String format uri", 167 | `{"type": "string", "format": "uri"}`, 168 | `"https://tools.ietf.org/html/rfc3986"`, 169 | }, 170 | { 171 | "String format uri-template", 172 | `{"type": "string", "format": "uri-template"}`, 173 | `"http://example.com/dictionary/{term:1}/{term}"`, 174 | }, 175 | { 176 | "String format json-pointer", 177 | `{"type": "string", "format": "json-pointer"}`, 178 | `"#/components/parameters/term"`, 179 | }, 180 | { 181 | "String format regex", 182 | `{"type": "string", "format": "regex"}`, 183 | `"/^1?$|^(11+?)\\1+$/"`, 184 | }, 185 | { 186 | "String format uuid", 187 | `{"type": "string", "format": "uuid"}`, 188 | `"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"`, 189 | }, 190 | { 191 | "String format password", 192 | `{"type": "string", "format": "password"}`, 193 | `"********"`, 194 | }, 195 | { 196 | "String min length", 197 | `{"type": "string", "minLength": 10}`, 198 | `"stringstring"`, 199 | }, 200 | { 201 | "String max length", 202 | `{"type": "string", "maxLength": 2}`, 203 | `"st"`, 204 | }, 205 | { 206 | "String min & max length", 207 | `{"type": "string", "minLength": 8, "maxLength": 10}`, 208 | `"stringstri"`, 209 | }, 210 | // ----- Arrays ----- 211 | { 212 | "Array without items returns empty []", 213 | `{ 214 | "type": "array" 215 | }`, 216 | `[]`, 217 | }, 218 | { 219 | "Array of simple type", 220 | `{ 221 | "type": "array", 222 | "items": { 223 | "type": "string" 224 | } 225 | }`, 226 | `["string"]`, 227 | }, 228 | { 229 | "Array of simple type with example", 230 | `{ 231 | "type": "array", 232 | "items": { 233 | "type": "string", 234 | "example": "I'm in an array" 235 | } 236 | }`, 237 | `["I'm in an array"]`, 238 | }, 239 | { 240 | "Array of blank items fails", 241 | `{ 242 | "type": "array", 243 | "items": {} 244 | }`, 245 | ``, 246 | }, 247 | { 248 | "Array with example", 249 | `{ 250 | "type": "array", 251 | "example": [true, false, true], 252 | "items": { 253 | "type": "boolean" 254 | } 255 | }`, 256 | `[true, false, true]`, 257 | }, 258 | { 259 | "Array of array of simple type", 260 | `{ 261 | "type": "array", 262 | "items": { 263 | "type": "array", 264 | "items": { 265 | "type": "number" 266 | } 267 | } 268 | }`, 269 | `[[0]]`, 270 | }, 271 | { 272 | "Array of objects", 273 | `{ 274 | "type": "array", 275 | "items": { 276 | "type": "object", 277 | "required": ["foo", "bar"], 278 | "properties": { 279 | "foo": { 280 | "type": "boolean" 281 | }, 282 | "bar": { 283 | "type": "string", 284 | "example": "baz" 285 | } 286 | } 287 | } 288 | }`, 289 | `[{"foo": true, "bar": "baz"}]`, 290 | }, 291 | { 292 | "Array with min items (e.g. coordinates)", 293 | `{ 294 | "type": "array", 295 | "minItems": 2, 296 | "items": { 297 | "type": "number" 298 | } 299 | }`, 300 | `[0, 0]`, 301 | }, 302 | // ----- Objects ----- 303 | { 304 | "Object without properties returns {}", 305 | `{ 306 | "type": "object" 307 | }`, 308 | `{}`, 309 | }, 310 | { 311 | "Object with example", 312 | `{ 313 | "type": "object", 314 | "example": { 315 | "foo": 1 316 | }, 317 | "properties": { 318 | "foo": { 319 | "type": "number" 320 | } 321 | } 322 | }`, 323 | `{"foo": 1}`, 324 | }, 325 | { 326 | "Object with simple properties", 327 | `{ 328 | "type": "object", 329 | "required": ["foo", "bar"], 330 | "properties": { 331 | "foo": { 332 | "type": "boolean" 333 | }, 334 | "bar": { 335 | "type": "string", 336 | "example": "baz" 337 | } 338 | } 339 | }`, 340 | `{"foo": true, "bar": "baz"}`, 341 | }, 342 | { 343 | "Object with complex properties", 344 | `{ 345 | "type": "object", 346 | "properties": { 347 | "foo": { 348 | "type": "object", 349 | "properties": { 350 | "bar": { 351 | "type": "array", 352 | "items": { 353 | "type": "string" 354 | } 355 | } 356 | } 357 | } 358 | } 359 | }`, 360 | `{"foo": {"bar": ["string"]}}`, 361 | }, 362 | { 363 | "Object with additional properties", 364 | `{ 365 | "type": "object", 366 | "properties": { 367 | "foo": { 368 | "type": "number" 369 | } 370 | }, 371 | "additionalProperties": { 372 | "type": "string" 373 | } 374 | }`, 375 | `{"foo": 0, "additionalPropertyName": "string"}`, 376 | }, 377 | { 378 | "Object with additional properties error", 379 | `{ 380 | "type": "object", 381 | "properties": { 382 | "foo": { 383 | "type": "number" 384 | } 385 | }, 386 | "additionalProperties": {} 387 | }`, 388 | ``, 389 | }, 390 | // ----- Precedence ----- 391 | { 392 | "Example before default", 393 | `{"type": "string", "default": "one", "example": "two"}`, 394 | `"two"`, 395 | }, 396 | // ----- Modes ----- 397 | { 398 | "Request mode", 399 | `{"type": "object", "required": ["normal", "readOnly", "writeOnly"], 400 | "properties": { 401 | "normal": { 402 | "type": "string" 403 | }, 404 | "readOnly": { 405 | "type": "string", 406 | "readOnly": true 407 | }, 408 | "writeOnly": { 409 | "type": "string", 410 | "writeOnly": true 411 | } 412 | } 413 | }`, 414 | `{"normal": "string", "writeOnly": "string"}`, 415 | }, 416 | { 417 | "Response mode", 418 | `{"type": "object", "required": ["normal", "readOnly", "writeOnly"], 419 | "properties": { 420 | "normal": { 421 | "type": "string" 422 | }, 423 | "readOnly": { 424 | "type": "string", 425 | "readOnly": true 426 | }, 427 | "writeOnly": { 428 | "type": "string", 429 | "writeOnly": true 430 | } 431 | } 432 | }`, 433 | `{"normal": "string", "readOnly": "string"}`, 434 | }, 435 | // ----- Combination keywords ----- 436 | { 437 | "Combine with allOf", 438 | `{ 439 | "allOf": [ 440 | { 441 | "type": "object", 442 | "properties": { 443 | "foo": {"type": "string"} 444 | } 445 | }, 446 | { 447 | "type": "object", 448 | "properties": { 449 | "bar": {"type": "boolean"} 450 | } 451 | } 452 | ] 453 | }`, 454 | `{"foo": "string", "bar": true}`, 455 | }, 456 | { 457 | "Combine with anyOf", 458 | `{ 459 | "anyOf": [ 460 | { 461 | "type": "object", 462 | "properties": { 463 | "foo": {"type": "string"} 464 | } 465 | }, 466 | { 467 | "type": "object", 468 | "properties": { 469 | "bar": {"type": "boolean"} 470 | } 471 | } 472 | ] 473 | }`, 474 | `{"foo": "string"}`, 475 | }, 476 | { 477 | "Combine with oneOf", 478 | `{ 479 | "oneOf": [ 480 | { 481 | "type": "object", 482 | "properties": { 483 | "foo": {"type": "string"} 484 | } 485 | }, 486 | { 487 | "type": "object", 488 | "properties": { 489 | "bar": {"type": "boolean"} 490 | } 491 | } 492 | ] 493 | }`, 494 | `{"foo": "string"}`, 495 | }, 496 | } 497 | 498 | func TestGenExample(t *testing.T) { 499 | for _, tt := range schemaTests { 500 | t.Run(tt.name, func(t *testing.T) { 501 | schema := &openapi3.Schema{} 502 | err := schema.UnmarshalJSON([]byte(tt.in)) 503 | assert.NoError(t, err) 504 | m := ModeRequest 505 | if strings.Contains(tt.name, "Response") { 506 | m = ModeResponse 507 | } 508 | example, err := OpenAPIExample(m, schema) 509 | 510 | if tt.out == "" { 511 | // Expected to return an error. 512 | assert.Nil(t, example) 513 | assert.Error(t, err) 514 | } else { 515 | // Expected to match the output. 516 | var expected interface{} 517 | json.Unmarshal([]byte(tt.out), &expected) 518 | assert.EqualValues(t, expected, example) 519 | } 520 | }) 521 | } 522 | } 523 | 524 | func TestRecursiveSchema(t *testing.T) { 525 | loader := openapi3.NewSwaggerLoader() 526 | 527 | tests := []struct { 528 | name string 529 | in string 530 | schema string 531 | out string 532 | }{ 533 | { 534 | "Valid recursive schema", 535 | exampleFixture(t, "recursive_ok.yml"), 536 | "Test", 537 | `{"something": "Hello"}`, 538 | }, 539 | { 540 | "Infinitely recursive schema", 541 | exampleFixture(t, "recursive_infinite.yml"), 542 | "Test", 543 | ``, 544 | }, 545 | { 546 | "Seeing the same schema twice non-recursively", 547 | exampleFixture(t, "recursive_seen_twice.yml"), 548 | "Test", 549 | `{"ref_a": {"spud": "potato"}, "ref_b": {"spud": "potato"}}`, 550 | }, 551 | { 552 | "Cyclical dependencies", 553 | exampleFixture(t, "recursive_cycles.yml"), 554 | "Front", 555 | ``, 556 | }, 557 | } 558 | for _, test := range tests { 559 | t.Run(test.name, func(t *testing.T) { 560 | swagger, err := loader.LoadSwaggerFromData([]byte(test.in)) 561 | require.NoError(t, err) 562 | 563 | ex, err := OpenAPIExample(ModeResponse, swagger.Components.Schemas[test.schema].Value) 564 | if test.out == "" { 565 | assert.Error(t, err) 566 | assert.Nil(t, ex) 567 | } else { 568 | assert.Nil(t, err) 569 | // Expected to match the output. 570 | var expected interface{} 571 | json.Unmarshal([]byte(test.out), &expected) 572 | assert.EqualValues(t, expected, ex) 573 | } 574 | }) 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danielgtaylor/apisprout 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | github.com/getkin/kin-openapi v0.2.0 8 | github.com/gobwas/glob v0.2.3 9 | github.com/magiconair/properties v1.8.1 // indirect 10 | github.com/pelletier/go-toml v1.4.0 // indirect 11 | github.com/pkg/errors v0.8.1 12 | github.com/spf13/afero v1.2.2 // indirect 13 | github.com/spf13/cobra v0.0.4 14 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 15 | github.com/spf13/pflag v1.0.3 16 | github.com/spf13/viper v1.4.0 17 | github.com/stretchr/testify v1.3.0 18 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect 19 | golang.org/x/text v0.3.2 // indirect 20 | gopkg.in/yaml.v2 v2.2.2 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 13 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 15 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 16 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 17 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 18 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 23 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 24 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 25 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 26 | github.com/getkin/kin-openapi v0.2.0 h1:PbHHtYZpjKwZtGlIyELgA2DploRrsaXztoNNx9HjwNY= 27 | github.com/getkin/kin-openapi v0.2.0/go.mod h1:V1z9xl9oF5Wt7v32ne4FmiF1alpS4dM6mNzoywPOXlk= 28 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 29 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 30 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 31 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 32 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 33 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 34 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 35 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 36 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 37 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 38 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 39 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 40 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 41 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 42 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 44 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 45 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 46 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 47 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 48 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 50 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 51 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 52 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 53 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 54 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 55 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 56 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 57 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 58 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 59 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 60 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 61 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 62 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 63 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 64 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 65 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 66 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 67 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 68 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 69 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 70 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 71 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 72 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 73 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 74 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 75 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 76 | github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= 77 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= 78 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 79 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 80 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 84 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 85 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 86 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 87 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 88 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 89 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 90 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 91 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 92 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 93 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 94 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 95 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 96 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 97 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 98 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 99 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 100 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 101 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 102 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 103 | github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE= 104 | github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 105 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 106 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 107 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 108 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 109 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 110 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 111 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 112 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 113 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 114 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 115 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 116 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 117 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 118 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 119 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 120 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 121 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 122 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 123 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 124 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 125 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 126 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 127 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 128 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 129 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 132 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 133 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 134 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 135 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 136 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 137 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 138 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 139 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 147 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU= 149 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 151 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 152 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 153 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 154 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 155 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 159 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 160 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 161 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 162 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 163 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 166 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 168 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 169 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 170 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 171 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 173 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0.0" 3 | description: Helm chart for apisprout 4 | name: "apisprout" 5 | version: 1.0.0 6 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This helm chart creates an apisprout deployment in a kubernetes cluster. Unfortunately it is not part of a helm repository yet, so you will have to download and install with the chart locally. 4 | 5 | 6 | ## Installation 7 | 8 | ``` 9 | helm install --name {release-name} --set apiyamlpath={open-api-yaml-path} {chart directory} 10 | ``` 11 | 12 | ## Uninstalling 13 | 14 | ``` 15 | helm delete --purge {release-name} 16 | ``` 17 | 18 | ## Values 19 | 20 | | Parameter | Description | Default | 21 | |:---------------------------|:--------------------------|:---------------------| 22 | | `apiyamlpath` | Path to OpenAPI Yaml file | Required: no default | 23 | | `appname` | The name of the app | apisprout | 24 | | `replicas` | Number of replicas | 1 | 25 | | `deployment.containerPort` | The container port | 8000 | 26 | | `service.annotations` | Service annotations | None | 27 | | `service.type` | The type of service | LoadBalancer | 28 | | `service.port` | The service port | 8000 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta2 2 | kind: Deployment 3 | metadata: 4 | name: "{{ .Values.appname }}" 5 | labels: 6 | app: "{{ .Values.appname }}" 7 | spec: 8 | replicas: {{ .Values.replicas }} 9 | strategy: 10 | type: RollingUpdate 11 | selector: 12 | matchLabels: 13 | app: "{{ .Values.appname }}" 14 | template: 15 | metadata: 16 | labels: 17 | app: "{{ .Values.appname }}" 18 | spec: 19 | containers: 20 | - name: "{{ .Values.appname }}" 21 | image: danielgtaylor/apisprout 22 | args: [ "{{required "The value .Values.apiyamlpath required!" .Values.apiyamlpath }}" ] 23 | ports: 24 | - name: http 25 | containerPort: {{ .Values.deployment.containerPort }} 26 | -------------------------------------------------------------------------------- /helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "{{ .Values.appname }}" 5 | {{- with .Values.service.annotations }} 6 | annotations: 7 | {{ toYaml . | indent 4 }} 8 | {{- end }} 9 | spec: 10 | type: {{ .Values.service.type }} 11 | selector: 12 | app: "{{ .Values.appname }}" 13 | ports: 14 | - port: {{ .Values.service.port }} 15 | targetPort: http 16 | 17 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values 2 | appname: apisprout 3 | replicas: 1 4 | 5 | deployment: 6 | containerPort: 8000 7 | 8 | service: 9 | type: LoadBalancer 10 | port: 8000 11 | 12 | -------------------------------------------------------------------------------- /logo.gvdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgtaylor/apisprout/55a1793d0b6bd5f6bc4f3c880cae61d2f83d81da/logo.gvdesign -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | govvv install 6 | VERSION=$(apisprout --version | cut -d ' ' -f3) 7 | 8 | GOOS=darwin GOARCH=amd64 govvv build 9 | tar -cJf apisprout-$VERSION-mac.tar.xz apisprout 10 | 11 | GOOS=linux GOARCH=amd64 govvv build 12 | tar -cJf apisprout-$VERSION-linux.tar.xz apisprout 13 | 14 | GOOS=windows GOARCH=amd64 govvv build 15 | zip -r apisprout-$VERSION-win-$GOARCH.zip apisprout.exe 16 | 17 | rm -f apisprout apisprout.exe 18 | -------------------------------------------------------------------------------- /testdata/example/recursive_cycles.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Front: 4 | type: object 5 | required: 6 | - back 7 | properties: 8 | back: 9 | $ref: '#/components/schemas/Back' 10 | Back: 11 | type: object 12 | required: 13 | - front 14 | properties: 15 | front: 16 | $ref: '#/components/schemas/Front' 17 | -------------------------------------------------------------------------------- /testdata/example/recursive_infinite.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Test: 4 | type: object 5 | required: 6 | - test 7 | properties: 8 | test: 9 | $ref: '#/components/schemas/Test' 10 | -------------------------------------------------------------------------------- /testdata/example/recursive_ok.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Test: 4 | type: object 5 | properties: 6 | something: 7 | type: string 8 | example: Hello 9 | test: 10 | $ref: '#/components/schemas/Test' 11 | -------------------------------------------------------------------------------- /testdata/example/recursive_seen_twice.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Ref: 4 | type: object 5 | properties: 6 | spud: 7 | type: string 8 | example: "potato" 9 | Test: 10 | type: object 11 | required: 12 | - ref_a 13 | - ref_b 14 | properties: 15 | ref_a: 16 | $ref: '#/components/schemas/Ref' 17 | ref_b: 18 | $ref: '#/components/schemas/Ref' 19 | --------------------------------------------------------------------------------