├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cmd └── hsup │ └── hsup.go ├── ext └── ext.go ├── go.mod ├── go.sum ├── hsup.go ├── httpclient └── httpclient.go ├── internal ├── genutil │ └── genutil.go └── parser │ └── parser.go ├── nethttp └── nethttp.go └── validator └── validator.go /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | install: 4 | - wget -q -O - https://github.com/Masterminds/glide/releases/download/0.10.2/glide-0.10.2-linux-amd64.tar.gz | tar zxf - 5 | - mv linux-amd64/glide . 6 | - rm -rf linux-amd64 7 | - ./glide install 8 | script: 9 | - go test -v $(./glide nv) 10 | go: 11 | - 1.6 12 | - tip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 lestrrat 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-hsup 2 | 3 | Generate scaffold web app from JSON Hyper Schema files 4 | 5 | # Synopsis 6 | 7 | Generate net/http flavored server code 8 | 9 | ```shell 10 | hsup -s /path/to/hyper-schema.json -f nethttp 11 | ``` 12 | 13 | Generate http.Client based client code 14 | 15 | ```shell 16 | hsup -s /path/to/hyper-schema.json -f httpclient 17 | ``` 18 | 19 | Generate both the net/http based server and the http client 20 | 21 | ```shell 22 | hsup -s /path/to/hyper-schema.json -f nethttp -f httpclient 23 | ``` 24 | 25 | # JSON Schema Additions 26 | 27 | Keys starting with `hsup.` are custom properties for hsup. 28 | 29 | | Key | Type | Description | 30 | |:--------------------|:-----------------------|:------------| 31 | | hsup.client | object | When specified at the top level, this is used to grab hints for generating client code | 32 | | hsup.client.imports | array(sring) | Specifies the list of additional code to import | 33 | | hsup.server | object | When specified at the top level, this is used to grab hints for generating server code | 34 | | hsup.server.imports | array(sring) | Specifies the list of additional code to import | 35 | | hsup.type | string | When specified within a link schema or targetSchema, this type is used to Marshal/Unmarshal data | 36 | | hsup.wrapper | string, arrray(string) | When specified within a link, the named function is used to wrap the HandleFunc. The signature for the wrapper must be `func(http.HandleFunc) http.HandleeFunc` | 37 | -------------------------------------------------------------------------------- /cmd/hsup/hsup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/jessevdk/go-flags" 10 | "github.com/lestrrat-go/hsup" 11 | "github.com/lestrrat-go/hsup/httpclient" 12 | "github.com/lestrrat-go/hsup/nethttp" 13 | "github.com/lestrrat-go/hsup/validator" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func main() { 18 | if err := _main(); err != nil { 19 | log.Printf("%s", err) 20 | os.Exit(1) 21 | } 22 | os.Exit(0) 23 | } 24 | 25 | func _main() error { 26 | // Remove every option that is prefixed 27 | prefixes := map[string][]string{ 28 | "nethttp": nil, 29 | "httpclient": nil, 30 | "validator": nil, 31 | } 32 | 33 | var mainargs []string 34 | OUTER: 35 | for i := 1; i < len(os.Args); i++ { 36 | v := os.Args[i] 37 | for prefix := range prefixes { 38 | // --prefix.localname=var or --prefix.localname var 39 | if !strings.HasPrefix(v, "--" + prefix + ".") { 40 | continue 41 | } 42 | 43 | localname := v[len(prefix)+3:] 44 | if len(localname) == 0 || localname[0] == '=' { 45 | return errors.New("prefixed parameter must have a local name: " + prefix) 46 | } 47 | 48 | l := prefixes[prefix] 49 | l = append(l, "--" + localname) 50 | if len(os.Args) > i + 1 { 51 | if nextv := os.Args[i+1]; len(nextv) > 0 && nextv[0] != '-' { 52 | l = append(l, os.Args[i+1]) 53 | i++ 54 | } 55 | } 56 | prefixes[prefix] = l 57 | continue OUTER 58 | } 59 | 60 | mainargs = append(mainargs, v) 61 | } 62 | 63 | var opts hsup.Options 64 | if _, err := flags.ParseArgs(&opts, mainargs); err != nil { 65 | return errors.Wrap(err, "failed to parse arguments") 66 | } 67 | 68 | // opts.Dir better be under GOPATH 69 | for _, path := range strings.Split(os.Getenv("GOPATH"), string([]rune{filepath.ListSeparator})) { 70 | path, err := filepath.Abs(path) 71 | if err != nil { 72 | return errors.Wrap(err, "failed to get absolute path") 73 | } 74 | path = filepath.Join(path, "src") 75 | dir, err := filepath.Abs(opts.Dir) 76 | if err != nil { 77 | return errors.Wrap(err, "failed to get absolute dir") 78 | } 79 | 80 | if strings.HasPrefix(dir, path) { 81 | opts.PkgPath = strings.TrimPrefix(strings.TrimPrefix(dir, path), string([]rune{filepath.Separator})) 82 | break 83 | } 84 | } 85 | 86 | if opts.PkgPath == "" { 87 | return errors.New("target path should be under GOPATH") 88 | } 89 | 90 | // Unless otherwise specified, last portion of the PkgPath is 91 | // the AppPkg 92 | if opts.AppPkg == "" { 93 | opts.AppPkg = filepath.Base(opts.PkgPath) 94 | } 95 | 96 | var cb func(hsup.Options) error 97 | for _, f := range opts.Flavor { 98 | log.Printf(" ===> running flavor '%s'", f) 99 | switch f { 100 | case "nethttp": 101 | cb = nethttp.Process 102 | case "httpclient": 103 | cb = httpclient.Process 104 | case "validator": 105 | cb = validator.Process 106 | default: 107 | return errors.New("unknown argument to `flavor`: " + f) 108 | } 109 | opts.Args = prefixes[f] 110 | 111 | if err := cb(opts); err != nil { 112 | return errors.Wrap(err, "failed to execute handler") 113 | } 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /ext/ext.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | const ( 4 | ClientMutateRequestKey = "hsup.client.mutate_request" 5 | CORSKey = "hsup.cors" 6 | MiddlewareKey = "hsup.middlewares" 7 | MultipartFilesKey = "hsup.multipartFiles" 8 | TypeKey = "hsup.type" 9 | TransportNsKey = "hsup.transport_ns" 10 | WrapperKey = "hsup.wrapper" 11 | ) 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/hsup 2 | 3 | require ( 4 | github.com/jessevdk/go-flags v1.4.0 5 | github.com/lestrrat-go/jshschema v0.0.0-20190212053720-8d17a4c5545e 6 | github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c // indirect 7 | github.com/lestrrat-go/jsref v0.0.0-20181205001954-1b590508f37d // indirect 8 | github.com/lestrrat-go/jsschema v0.0.0-20181205002244-5c81c58ffcc3 9 | github.com/lestrrat-go/jsval v0.0.0-20181205002323-20277e9befc0 10 | github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe // indirect 11 | github.com/lestrrat-go/structinfo v0.0.0-20160308131105-f74c056fe41f // indirect 12 | github.com/pkg/errors v0.8.1 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 2 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 3 | github.com/lestrrat-go/jshschema v0.0.0-20190212053720-8d17a4c5545e h1:wJJPb6F8/0F0nf/NbtnGZMss/Jt8875kx8a8WnhCMJE= 4 | github.com/lestrrat-go/jshschema v0.0.0-20190212053720-8d17a4c5545e/go.mod h1:3UsGKF1/Z+gvY0UIBiryY4TvtPQWJk6UCFm65/0jIk4= 5 | github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c h1:pGh5EFIfczeDHwgMHgfwjhZzL+8/E3uZF6T7vER/W8c= 6 | github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c/go.mod h1:xw2Gm4Mg+ST9s8fHR1VkUIyOJMJnSloRZlPQB+wyVpY= 7 | github.com/lestrrat-go/jsref v0.0.0-20181205001954-1b590508f37d h1:1eeFdKL5ySmmYevvKv7iECIc4dTATeKTtBqP4/nXxDk= 8 | github.com/lestrrat-go/jsref v0.0.0-20181205001954-1b590508f37d/go.mod h1:h+r25adx46+IvUSt/rTTvXNnCDnu3lRTkMPPR/GdCwk= 9 | github.com/lestrrat-go/jsschema v0.0.0-20181205002244-5c81c58ffcc3 h1:TSKrrGm89gmmVlrG34ZzCIOMNVk5kkSV1P88Dt38DiE= 10 | github.com/lestrrat-go/jsschema v0.0.0-20181205002244-5c81c58ffcc3/go.mod h1:SVfIykmWQyFuRToBTKQ8AcveWeOunS2phYxA8hJ/6Gg= 11 | github.com/lestrrat-go/jsval v0.0.0-20181205002323-20277e9befc0 h1:w4rIjeCV/gQpxtn3i1voyF6Hd7v1mRGIB63F7RZOk1U= 12 | github.com/lestrrat-go/jsval v0.0.0-20181205002323-20277e9befc0/go.mod h1:hazjwMAn+trtmUnjvhIzSIZ0YS+2egAMonQMjDhcC2s= 13 | github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe h1:S7XSBlgc/eI2v47LkPPVa+infH3FuTS4tPJbqCtJovo= 14 | github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe/go.mod h1:zvUY6gZZVL2nu7NM+/3b51Z/hxyFZCZxV0hvfZ3NJlg= 15 | github.com/lestrrat-go/structinfo v0.0.0-20160308131105-f74c056fe41f h1:EDWKHzV8wIviXmlKSkCl/VIqSfPY90EaG//J3R4LprQ= 16 | github.com/lestrrat-go/structinfo v0.0.0-20160308131105-f74c056fe41f/go.mod h1:s2U6PowV3/Jobkx/S9d0XiPwOzs6niW3DIouw+7nZC8= 17 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 18 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | -------------------------------------------------------------------------------- /hsup.go: -------------------------------------------------------------------------------- 1 | package hsup 2 | 3 | // Package hsup processes JSON Hyper Schema files to generated 4 | // skeleton web applications. 5 | // 6 | // /* generate net/http compliant code */ 7 | // hsup.NetHTTP.ProcessFile(schemaFile) 8 | 9 | import ( 10 | "github.com/lestrrat-go/jshschema" 11 | ) 12 | 13 | type Processor interface { 14 | Process(*hschema.HyperSchema) error 15 | ProcessFile(string) error 16 | } 17 | 18 | type Options struct { 19 | Dir string `short:"d" long:"dir" required:"true" description:"Directory to place all files under"` 20 | PkgPath string 21 | AppPkg string `short:"a" long:"apppkg" description:"Application package name"` 22 | Schema string `short:"s" long:"schema" required:"true" description:"schema file to process"` 23 | Flavor []string `short:"f" long:"flavor" default:"nethttp" default:"validator" default:"httpclient" description:"what type of code to generate"` 24 | Overwrite bool `short:"O" long:"overwrite" description:"overwrite if file exists"` 25 | GoVersion string `short:"g" long:"goversion" description:"Go version to assume" default:"1.7"` 26 | Args []string // left over arguments 27 | } 28 | -------------------------------------------------------------------------------- /httpclient/httpclient.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/jessevdk/go-flags" 15 | "github.com/lestrrat-go/hsup" 16 | "github.com/lestrrat-go/hsup/ext" 17 | "github.com/lestrrat-go/hsup/internal/genutil" 18 | "github.com/lestrrat-go/hsup/internal/parser" 19 | "github.com/lestrrat-go/jshschema" 20 | "github.com/pkg/errors" 21 | ) 22 | 23 | type Builder struct { 24 | AppPkg string 25 | ClientPkg string 26 | Dir string 27 | Overwrite bool 28 | PkgPath string 29 | } 30 | 31 | type clientHints struct { 32 | Imports []string 33 | } 34 | 35 | type genctx struct { 36 | *parser.Result 37 | AppPkg string 38 | ClientHints clientHints 39 | ClientPkg string 40 | Dir string 41 | Overwrite bool 42 | PkgPath string 43 | } 44 | 45 | type options struct { 46 | } 47 | 48 | func Process(opts hsup.Options) error { 49 | var localopts options 50 | if _, err := flags.ParseArgs(&localopts, opts.Args); err != nil { 51 | return errors.Wrap(err, "failed to parse command line arguments") 52 | } 53 | 54 | b := New() 55 | b.Dir = opts.Dir 56 | b.AppPkg = opts.AppPkg 57 | b.PkgPath = opts.PkgPath 58 | b.Overwrite = opts.Overwrite 59 | if err := b.ProcessFile(opts.Schema); err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | func New() *Builder { 66 | return &Builder{ 67 | AppPkg: "app", 68 | ClientPkg: "client", 69 | Overwrite: false, 70 | } 71 | } 72 | 73 | func (b *Builder) ProcessFile(f string) error { 74 | log.Printf(" ===> Using schema file '%s'", f) 75 | s, err := hschema.ReadFile(f) 76 | if err != nil { 77 | return err 78 | } 79 | return b.Process(s) 80 | } 81 | 82 | func (b *Builder) Process(s *hschema.HyperSchema) error { 83 | ctx := genctx{ 84 | AppPkg: b.AppPkg, 85 | ClientPkg: b.ClientPkg, 86 | Dir: b.Dir, 87 | Overwrite: b.Overwrite, 88 | PkgPath: b.PkgPath, 89 | } 90 | 91 | if err := parse(&ctx, s); err != nil { 92 | return err 93 | } 94 | 95 | if err := generateFiles(&ctx); err != nil { 96 | return err 97 | } 98 | 99 | log.Printf(" <=== All files generated") 100 | return nil 101 | } 102 | 103 | func parseClientHints(ctx *genctx, m map[string]interface{}) error { 104 | if v, ok := m["imports"]; ok { 105 | switch v.(type) { 106 | case []interface{}: 107 | default: 108 | return errors.New("invalid value type for imports: expected []interface{}") 109 | } 110 | 111 | l := v.([]interface{}) 112 | ctx.ClientHints.Imports = make([]string, len(l)) 113 | for i, n := range l { 114 | switch n.(type) { 115 | case string: 116 | default: 117 | return errors.New("invalid value type for elements in imports: expected string") 118 | } 119 | ctx.ClientHints.Imports[i] = n.(string) 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | func parseExtras(ctx *genctx, s *hschema.HyperSchema) error { 126 | for k, v := range s.Extras { 127 | switch k { 128 | case "hsup.client": 129 | switch v.(type) { 130 | case map[string]interface{}: 131 | default: 132 | return errors.New("invalid value type for hsup.client: expected map[string]interface{}") 133 | } 134 | 135 | if err := parseClientHints(ctx, v.(map[string]interface{})); err != nil { 136 | return err 137 | } 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | func parse(ctx *genctx, s *hschema.HyperSchema) error { 144 | pres, err := parser.Parse(s) 145 | if err != nil { 146 | return err 147 | } 148 | ctx.Result = pres 149 | 150 | if err := parseExtras(ctx, s); err != nil { 151 | return err 152 | } 153 | 154 | for _, link := range s.Links { 155 | methodName := genutil.TitleToName(link.Title) 156 | methodBody, err := makeMethod(ctx, methodName, link) 157 | if err != nil { 158 | return err 159 | } 160 | ctx.Methods[methodName] = methodBody 161 | } 162 | 163 | sort.Strings(ctx.MethodNames) 164 | return nil 165 | } 166 | 167 | func makeMethod(ctx *genctx, name string, l *hschema.Link) (string, error) { 168 | intype := "" 169 | outtype := "" 170 | if s := l.Schema; s != nil { 171 | if !s.IsResolved() { 172 | rs, err := s.Resolve(ctx.Schema) 173 | if err != nil { 174 | return "", err 175 | } 176 | s = rs 177 | } 178 | intype = "interface{}" 179 | if t, ok := ctx.RequestPayloadType[name]; ok { 180 | intype = t 181 | } 182 | } 183 | 184 | if s := l.TargetSchema; s != nil { 185 | if !s.IsResolved() { 186 | rs, err := s.Resolve(ctx.Schema) 187 | if err != nil { 188 | return "", err 189 | } 190 | s = rs 191 | } 192 | outtype = "interface{}" 193 | if t, ok := ctx.ResponsePayloadType[name]; ok { 194 | outtype = t 195 | } 196 | } 197 | 198 | buf := bytes.Buffer{} 199 | fmt.Fprintf(&buf, `func (c *Client) %s(`, name) 200 | if intype != "" { 201 | buf.WriteString("in ") 202 | if genutil.LooksLikeStruct(intype) { 203 | buf.WriteRune('*') 204 | } 205 | buf.WriteString(intype) 206 | } 207 | 208 | // If this is a multipart/form-data link, we need to add the potential 209 | // files. This will be specified as a map of strings 210 | var files []string 211 | if extv, ok := l.Extras[ext.MultipartFilesKey]; ok { 212 | listv, ok := extv.([]interface{}) 213 | if !ok { 214 | return "", errors.Errorf("'%s' key must be a []string", ext.MultipartFilesKey) 215 | } 216 | files = make([]string, len(listv)) 217 | for i, v := range listv { 218 | sv, ok := v.(string) 219 | if !ok { 220 | return "", errors.Errorf("'%s' key must be a []string", ext.MultipartFilesKey) 221 | } 222 | files[i] = sv 223 | } 224 | 225 | if intype != "" { 226 | buf.WriteString(", ") 227 | } 228 | 229 | buf.WriteString("files map[string]string") 230 | } 231 | 232 | buf.WriteRune(')') 233 | 234 | if outtype == "" { 235 | buf.WriteString(`(err error) {`) 236 | } else { 237 | prefix := "" 238 | if genutil.LooksLikeStruct(outtype) { 239 | prefix = "*" 240 | } 241 | 242 | fmt.Fprintf(&buf, `(ret %s%s, err error) {`, prefix, outtype) 243 | } 244 | 245 | buf.WriteString("\nif pdebug.Enabled {") 246 | fmt.Fprintf(&buf, "\ng := pdebug.Marker(%s).BindError(&err)", strconv.Quote("client."+name)) 247 | buf.WriteString("\ndefer g.End()") 248 | buf.WriteString("\n}") 249 | 250 | errbuf := bytes.Buffer{} 251 | errbuf.WriteString("\nif err != nil {") 252 | if outtype == "" { 253 | errbuf.WriteString("\nreturn err") 254 | } else { 255 | errbuf.WriteString("\nreturn nil, err") 256 | } 257 | errbuf.WriteString("\n}") 258 | errout := errbuf.String() 259 | 260 | fmt.Fprintf(&buf, "\n"+`u, err := url.Parse(c.endpoint + %s)`, strconv.Quote(l.Path())) 261 | buf.WriteString(errout) 262 | 263 | method := strings.ToLower(l.Method) 264 | if method == "" { 265 | method = "get" 266 | } 267 | if _, ok := ctx.RequestPayloadType[name]; ok { 268 | if method == "get" { 269 | buf.WriteString("\nbuf, err := urlenc.Marshal(in)") 270 | buf.WriteString(errout) 271 | buf.WriteString("\nu.RawQuery = string(buf)") 272 | } else { 273 | buf.WriteString("\nvar buf bytes.Buffer") 274 | if l.EncType == "multipart/form-data" { 275 | buf.WriteString("\nw := multipart.NewWriter(&buf)") 276 | buf.WriteString("\nvar jsbuf bytes.Buffer") 277 | buf.WriteString("\nerr = json.NewEncoder(&jsbuf).Encode(in)") 278 | buf.WriteString(errout) 279 | buf.WriteString("\nw.WriteField(\"payload\", jsbuf.String())") 280 | 281 | // files are specified outside of the schema, because they are not 282 | // to be validated 283 | for _, name := range files { 284 | fmt.Fprintf(&buf, "\nif fn, ok := files[%s]; ok {", strconv.Quote(name)) 285 | fmt.Fprintf(&buf, "\nfw, err := w.CreateFormFile(%s, fn)", strconv.Quote(name)) 286 | buf.WriteString(errout) 287 | buf.WriteString("\nf, err := os.Open(fn)") 288 | buf.WriteString(errout) 289 | buf.WriteString("\ndefer f.Close()") 290 | buf.WriteString("\n_, err = io.Copy(fw, f)") 291 | buf.WriteString(errout) 292 | buf.WriteString("\n}") 293 | } 294 | buf.WriteString("\nerr = w.Close()") 295 | buf.WriteString(errout) 296 | } else { 297 | buf.WriteString("\n" + `err = json.NewEncoder(&buf).Encode(in)`) 298 | buf.WriteString(errout) 299 | } 300 | } 301 | } 302 | 303 | switch method { 304 | case "get": 305 | buf.WriteString("\nif pdebug.Enabled {") 306 | fmt.Fprintf(&buf, "\npdebug.Printf(%s, u.String())", strconv.Quote("GET to %s")) 307 | buf.WriteString("\n}") 308 | buf.WriteString("\n" + `req, err := http.NewRequest("GET", u.String(), nil)`) 309 | buf.WriteString(errout) 310 | case "post": 311 | buf.WriteString("\nif pdebug.Enabled {") 312 | fmt.Fprintf(&buf, "\npdebug.Printf(%s, u.String())", strconv.Quote("POST to %s")) 313 | buf.WriteString("\n" + `pdebug.Printf("%s", buf.String())`) 314 | buf.WriteString("\n}") 315 | buf.WriteString("\n" + `req, err := http.NewRequest("POST", u.String(), &buf)`) 316 | buf.WriteString(errout) 317 | 318 | if l.EncType == "multipart/form-data" { 319 | // Must create a multipart/form-data request 320 | buf.WriteString("\nreq.Header.Set(\"Content-Type\", w.FormDataContentType())") 321 | } else { 322 | buf.WriteString("\n" + `req.Header.Set("Content-Type", "application/json")`) 323 | } 324 | } 325 | 326 | buf.WriteString("\n" + `if c.basicAuth.username != "" && c.basicAuth.password != "" {`) 327 | buf.WriteString("\nreq.SetBasicAuth(c.basicAuth.username, c.basicAuth.password)") 328 | buf.WriteString("\n}") 329 | buf.WriteString("\n\nif m := c.mutator; m != nil {") 330 | buf.WriteString("\nif err := m(req); err != nil {") 331 | buf.WriteString("\nreturn ") 332 | if outtype != "" { 333 | buf.WriteString("nil, ") 334 | } 335 | buf.WriteString("errors.Wrap(err, `failed to mutate request`)") 336 | buf.WriteString("\n}") 337 | buf.WriteString("\n}") 338 | buf.WriteString("\n" + `res, err := c.client.Do(req)`) 339 | buf.WriteString(errout) 340 | 341 | buf.WriteString("\nif res.StatusCode != http.StatusOK {") 342 | // If in case of an error, we should at least attempt to parse the 343 | // resulting JSON 344 | buf.WriteString("\nif strings.HasPrefix(strings.ToLower(res.Header.Get(`Content-Type`)), `application/json`) {") 345 | buf.WriteString("\nvar errjson ErrJSON") 346 | buf.WriteString("\nif err := json.NewDecoder(res.Body).Decode(&errjson); err != nil {") 347 | buf.WriteString("\nreturn ") 348 | if outtype != "" { 349 | buf.WriteString("nil, ") 350 | } 351 | buf.WriteString("errors.Errorf(`Invalid response: '%s'`, res.Status)") 352 | buf.WriteString("\n}") 353 | 354 | buf.WriteString("\nif len(errjson.Error) > 0 {") 355 | buf.WriteString("\nreturn ") 356 | if outtype != "" { 357 | buf.WriteString("nil, ") 358 | } 359 | buf.WriteString("errors.New(errjson.Error)") 360 | buf.WriteString("\n}") 361 | buf.WriteString("\n}") 362 | buf.WriteString("\nreturn ") 363 | if outtype != "" { 364 | buf.WriteString("nil, ") 365 | } 366 | buf.WriteString("errors.Errorf(`Invalid response: '%s'`, res.Status)") 367 | buf.WriteString("\n}") 368 | if outtype == "" { 369 | buf.WriteString("\nreturn nil") 370 | } else { 371 | 372 | buf.WriteString("\njsonbuf := getTransportJSONBuffer()") 373 | buf.WriteString("\ndefer releaseTransportJSONBuffer(jsonbuf)") 374 | buf.WriteString("\n_, err = io.Copy(jsonbuf, io.LimitReader(res.Body, MaxResponseSize))") 375 | buf.WriteString("\ndefer res.Body.Close()") 376 | buf.WriteString("\nif pdebug.Enabled {") 377 | buf.WriteString("\nif err != nil {") 378 | buf.WriteString("\n" + `pdebug.Printf("failed to read respons buffer: %s", err)`) 379 | buf.WriteString("\n} else {") 380 | buf.WriteString("\n" + `pdebug.Printf("response buffer: %s", jsonbuf)`) 381 | buf.WriteString("\n}") 382 | buf.WriteString("\n}") 383 | buf.WriteString(errout) 384 | buf.WriteString("\n\nvar payload ") 385 | buf.WriteString(outtype) 386 | buf.WriteString("\nerr = json.Unmarshal(jsonbuf.Bytes(), &payload)") 387 | buf.WriteString(errout) 388 | buf.WriteString("\nreturn ") 389 | if genutil.LooksLikeStruct(outtype) { 390 | buf.WriteString("&") 391 | } 392 | buf.WriteString("payload, nil") 393 | } 394 | buf.WriteString("\n}") 395 | 396 | return buf.String(), nil 397 | } 398 | 399 | func generateFile(ctx *genctx, fn string, cb func(io.Writer, *genctx) error) error { 400 | if _, err := os.Stat(fn); err == nil { 401 | if !ctx.Overwrite { 402 | log.Printf(" - File '%s' already exists. Skipping", fn) 403 | return nil 404 | } 405 | log.Printf(" * File '%s' already exists. Overwriting", fn) 406 | } 407 | 408 | log.Printf(" + Generating file '%s'", fn) 409 | f, err := genutil.CreateFile(fn) 410 | if err != nil { 411 | return err 412 | } 413 | defer f.Close() 414 | return cb(f, ctx) 415 | } 416 | 417 | func generateFiles(ctx *genctx) error { 418 | { 419 | fn := filepath.Join(ctx.Dir, "client", "client.go") 420 | if err := generateFile(ctx, fn, generateClientCode); err != nil { 421 | return err 422 | } 423 | } 424 | 425 | return nil 426 | } 427 | 428 | func generateClientCode(out io.Writer, ctx *genctx) error { 429 | buf := bytes.Buffer{} 430 | 431 | genutil.WriteDoNotEdit(&buf) 432 | fmt.Fprintf(&buf, "package %s\n\n", ctx.ClientPkg) 433 | 434 | imports := []string{"github.com/lestrrat-go/pdebug", "github.com/lestrrat-go/urlenc", "github.com/pkg/errors"} 435 | if l := ctx.ClientHints.Imports; len(l) > 0 { 436 | imports = append(imports, l...) 437 | } 438 | 439 | genutil.WriteImports( 440 | &buf, 441 | []string{"bytes", "encoding/json", "io", "mime/multipart", "net/http", "net/url", "os", "strings", "sync"}, 442 | imports, 443 | ) 444 | 445 | buf.WriteString(` 446 | const MaxResponseSize = (1<<20)*2 447 | var _ = bytes.MinRead 448 | var _ = json.Decoder{} 449 | var _ = multipart.Form{} 450 | var _ = os.Stdout 451 | var transportJSONBufferPool = sync.Pool{ 452 | New: allocTransportJSONBuffer, 453 | } 454 | 455 | func allocTransportJSONBuffer() interface {} { 456 | return &bytes.Buffer{} 457 | } 458 | 459 | func getTransportJSONBuffer() *bytes.Buffer { 460 | return transportJSONBufferPool.Get().(*bytes.Buffer) 461 | } 462 | 463 | func releaseTransportJSONBuffer(buf *bytes.Buffer) { 464 | buf.Reset() 465 | transportJSONBufferPool.Put(buf) 466 | } 467 | 468 | type BasicAuth struct { 469 | username string 470 | password string 471 | } 472 | 473 | func (a BasicAuth) Username() string { 474 | return a.username 475 | } 476 | 477 | func (a BasicAuth) Password() string { 478 | return a.password 479 | } 480 | 481 | type ErrJSON struct { 482 | Error string ` + "`" + `json:"error,omitempty"` + "`" + ` 483 | } 484 | 485 | type Client struct { 486 | basicAuth BasicAuth 487 | client *http.Client 488 | endpoint string 489 | mutator func(*http.Request) error 490 | } 491 | 492 | func New(s string) *Client { 493 | return &Client{ 494 | client: &http.Client{}, 495 | endpoint: s, 496 | } 497 | } 498 | 499 | func (c *Client) BasicAuth() BasicAuth { 500 | return c.basicAuth 501 | } 502 | 503 | func (c *Client) SetAuth(username, password string) { 504 | c.basicAuth.username = username 505 | c.basicAuth.password = password 506 | } 507 | 508 | func (c *Client) Client() *http.Client { 509 | return c.client 510 | } 511 | 512 | func (c *Client) Endpoint() string { 513 | return c.endpoint 514 | } 515 | 516 | func (c *Client) SetMutator(m func(*http.Request) error) { 517 | c.mutator = m 518 | } 519 | 520 | `) 521 | 522 | // for each endpoint, create a method that accepts 523 | for _, methodName := range ctx.MethodNames { 524 | method := ctx.Methods[methodName] 525 | fmt.Fprint(&buf, method) 526 | fmt.Fprint(&buf, "\n\n") 527 | } 528 | 529 | if err := genutil.WriteFmtCode(out, &buf); err != nil { 530 | return err 531 | } 532 | 533 | return nil 534 | } 535 | -------------------------------------------------------------------------------- /internal/genutil/genutil.go: -------------------------------------------------------------------------------- 1 | package genutil 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/lestrrat-go/jsschema" 16 | "github.com/lestrrat-go/jsval" 17 | "github.com/lestrrat-go/jsval/builder" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | var rxif = regexp.MustCompile(`\s*interface\s*{\s*}\s*`) 22 | 23 | func LooksLikeContainer(s string) bool { 24 | return strings.HasPrefix(s, "[]") || strings.HasPrefix(s, "map[") 25 | } 26 | 27 | func LooksLikeStruct(s string) bool { 28 | if rxif.MatchString(s) { 29 | return false 30 | } 31 | return !LooksLikeContainer(s) 32 | } 33 | 34 | var wsrx = regexp.MustCompile(`\s+`) 35 | 36 | func TitleToName(s string) string { 37 | buf := bytes.Buffer{} 38 | for _, p := range wsrx.Split(s, -1) { 39 | buf.WriteString(strings.ToUpper(p[:1])) 40 | buf.WriteString(p[1:]) 41 | } 42 | return buf.String() 43 | } 44 | 45 | func MakeValidator(s *schema.Schema, ctx interface{}) (*jsval.JSVal, error) { 46 | b := builder.New() 47 | v, err := b.BuildWithCtx(s, ctx) 48 | if err != nil { 49 | return nil, errors.Wrap(err, "failed to build validator from schema") 50 | } 51 | 52 | return v, nil 53 | } 54 | 55 | func WriteImports(out io.Writer, stdlibs, extlibs []string) error { 56 | if len(stdlibs) == 0 && len(extlibs) == 0 { 57 | return nil 58 | } 59 | 60 | fmt.Fprint(out, "import (\n") 61 | for _, pname := range stdlibs { 62 | fmt.Fprint(out, "\t"+`"`+pname+`"`+"\n") 63 | } 64 | if len(extlibs) > 0 { 65 | if len(stdlibs) > 0 { 66 | fmt.Fprint(out, "\n") 67 | } 68 | for _, pname := range extlibs { 69 | fmt.Fprint(out, "\t"+`"`+pname+`"`+"\n") 70 | } 71 | } 72 | fmt.Fprint(out, ")\n\n") 73 | return nil 74 | } 75 | 76 | func CreateFile(fn string) (*os.File, error) { 77 | dir := filepath.Dir(fn) 78 | if _, err := os.Stat(dir); err != nil { 79 | if err := os.MkdirAll(dir, 0755); err != nil { 80 | return nil, errors.Wrap(err, "failed to create directory") 81 | } 82 | } 83 | f, err := os.Create(fn) 84 | if err != nil { 85 | return nil, errors.Wrap(err, "failed to create file") 86 | } 87 | return f, nil 88 | } 89 | 90 | func WriteFmtCode(out io.Writer, buf *bytes.Buffer) error { 91 | fsrc, err := format.Source(buf.Bytes()) 92 | if err != nil { 93 | log.Printf("Failed to cleanup Go code (probably a syntax error). Generating file anyway") 94 | if _, err := buf.WriteTo(out); err != nil { 95 | return errors.Wrap(err, "failed to write (broken) source to output") 96 | } 97 | return nil 98 | } 99 | 100 | if _, err := out.Write(fsrc); err != nil { 101 | return errors.Wrap(err, "failed to write to output") 102 | } 103 | return nil 104 | } 105 | 106 | func WriteDoNotEdit(out io.Writer) { 107 | fmt.Fprintf(out, "// DO NOT EDIT. Automatically generated by hsup\n") 108 | } 109 | 110 | func SplitVersion(v string) []int { 111 | ret := make([]int, 3) 112 | list := strings.Split(v, ".") 113 | if len(list) > 3 { 114 | return ret 115 | } 116 | 117 | for i, e := range list[:2] { 118 | x, _ := strconv.Atoi(e) 119 | ret[i] = x 120 | } 121 | return ret 122 | } 123 | 124 | func VersionCompare(v1, v2 string) int { 125 | e1 := SplitVersion(v1) 126 | e2 := SplitVersion(v2) 127 | 128 | for i := 0; i < 3; i++ { 129 | if e1[i] == e2[i] { 130 | continue 131 | } 132 | 133 | if e1[i] > e2[i] { 134 | return -1 135 | } 136 | if e1[i] < e2[i] { 137 | return 1 138 | } 139 | } 140 | return 0 141 | } 142 | -------------------------------------------------------------------------------- /internal/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/lestrrat-go/hsup/ext" 10 | "github.com/lestrrat-go/hsup/internal/genutil" 11 | "github.com/lestrrat-go/jshschema" 12 | "github.com/lestrrat-go/jsval" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type Result struct { 17 | Schema *hschema.HyperSchema 18 | Methods map[string]string 19 | MethodNames []string 20 | MethodWrappers map[string][]string 21 | Middlewares []string 22 | PathToMethods map[string]string 23 | RequestCORS map[string]string 24 | RequestMutators map[string][]string 25 | RequestPayloadType map[string]string 26 | RequestValidators map[string]*jsval.JSVal 27 | ResponsePayloadType map[string]string 28 | ResponseValidators map[string]*jsval.JSVal 29 | } 30 | 31 | func Parse(s *hschema.HyperSchema) (*Result, error) { 32 | ctx := Result{ 33 | Schema: s, 34 | MethodNames: make([]string, len(s.Links)), 35 | Methods: make(map[string]string), 36 | MethodWrappers: make(map[string][]string), 37 | PathToMethods: make(map[string]string), 38 | RequestCORS: make(map[string]string), 39 | RequestMutators: make(map[string][]string), 40 | RequestPayloadType: make(map[string]string), 41 | RequestValidators: make(map[string]*jsval.JSVal), 42 | ResponseValidators: make(map[string]*jsval.JSVal), 43 | ResponsePayloadType: make(map[string]string), 44 | } 45 | 46 | if err := parse(&ctx, s); err != nil { 47 | return nil, errors.Wrap(err, "failed to parse JSON hyper schema") 48 | } 49 | return &ctx, nil 50 | } 51 | 52 | func parse(ctx *Result, s *hschema.HyperSchema) error { 53 | middlewares, ok := s.Extras[ext.MiddlewareKey] 54 | if ok { 55 | var mwlist []interface{} 56 | mwlist, ok = middlewares.([]interface{}) 57 | if ok { 58 | ctx.Middlewares = make([]string, len(mwlist)) 59 | for i, mw := range mwlist { 60 | ctx.Middlewares[i] = mw.(string) 61 | } 62 | } 63 | } 64 | 65 | // We want to know the namespace of the transport. 66 | // Normally we just use "model" 67 | transportNs, ok := s.Extras[ext.TransportNsKey] 68 | if !ok { 69 | transportNs = "model" 70 | } 71 | for i, link := range s.Links { 72 | if len(link.Title) == 0 { 73 | return errors.New("link " + strconv.Itoa(i) + ": hsup requires a 'title' element to generate resources") 74 | } 75 | 76 | methodName := genutil.TitleToName(link.Title) 77 | 78 | if v, ok := link.Extras[ext.CORSKey]; ok { 79 | ctx.RequestCORS[methodName] = v.(string) 80 | } 81 | 82 | if cmr, ok := link.Extras[ext.ClientMutateRequestKey]; ok { 83 | switch cmr.(type) { 84 | case string: 85 | ctx.RequestMutators[methodName] = []string{cmr.(string)} 86 | case []interface{}: 87 | list, ok := cmr.([]interface{}) 88 | if !ok { 89 | return errors.Errorf(`%s must be a string or a list of strings`, ext.ClientMutateRequestKey) 90 | } 91 | cmrs := make([]string, len(list)) 92 | for i, e := range list { 93 | s, ok := e.(string) 94 | if !ok { 95 | return errors.Errorf(`%s must be a string or a list of strings`, ext.ClientMutateRequestKey) 96 | } 97 | cmrs[i] = s 98 | } 99 | ctx.RequestMutators[methodName] = cmrs 100 | default: 101 | return errors.Errorf(`%s must be a string or a list of strings`, ext.ClientMutateRequestKey) 102 | } 103 | } 104 | // Got to do this first, because validators are used in makeMethod() 105 | if ls := link.Schema; ls != nil { 106 | if !ls.IsResolved() { 107 | rs, err := ls.Resolve(ctx.Schema) 108 | if err != nil { 109 | return errors.Wrap(err, "failed to resolve schema (request)") 110 | } 111 | ls = rs 112 | } 113 | v, err := genutil.MakeValidator(ls, ctx.Schema) 114 | if err != nil { 115 | return errors.Wrap(err, "failed to create request validator") 116 | } 117 | 118 | if gt, ok := ls.Extras[ext.TypeKey]; ok { 119 | ctx.RequestPayloadType[methodName] = gt.(string) 120 | } else { 121 | ctx.RequestPayloadType[methodName] = fmt.Sprintf("%s.%sRequest", transportNs, methodName) 122 | } 123 | v.Name = fmt.Sprintf("HTTP%sRequest", methodName) 124 | ctx.RequestValidators[methodName] = v 125 | 126 | } 127 | 128 | if ls := link.TargetSchema; ls != nil { 129 | if !ls.IsResolved() { 130 | rs, err := ls.Resolve(ctx.Schema) 131 | if err != nil { 132 | return errors.Wrap(err, "failed to resolve target schema (response)") 133 | } 134 | ls = rs 135 | } 136 | v, err := genutil.MakeValidator(ls, ctx.Schema) 137 | if err != nil { 138 | return errors.Wrap(err, "failed to create response validator") 139 | } 140 | ctx.ResponsePayloadType[methodName] = "interface{}" 141 | if gt, ok := ls.Extras[ext.TypeKey]; ok { 142 | ctx.ResponsePayloadType[methodName] = gt.(string) 143 | } else { 144 | ctx.ResponsePayloadType[methodName] = fmt.Sprintf("%s.%sResponse", transportNs, methodName) 145 | } 146 | v.Name = fmt.Sprintf("HTTP%sResponse", methodName) 147 | ctx.ResponseValidators[methodName] = v 148 | } 149 | 150 | ctx.MethodNames[i] = methodName 151 | path := link.Path() 152 | if strings.IndexRune(path, '{') > -1 { 153 | return errors.New("found '{' in the URL. hsup does not support URI templates") 154 | } 155 | ctx.PathToMethods[path] = methodName 156 | 157 | } 158 | sort.Strings(ctx.MethodNames) 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /nethttp/nethttp.go: -------------------------------------------------------------------------------- 1 | package nethttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/jessevdk/go-flags" 15 | "github.com/lestrrat-go/hsup" 16 | "github.com/lestrrat-go/hsup/ext" 17 | "github.com/lestrrat-go/hsup/internal/genutil" 18 | "github.com/lestrrat-go/hsup/internal/parser" 19 | "github.com/lestrrat-go/jshschema" 20 | "github.com/lestrrat-go/jsschema" 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | type Builder struct { 25 | AppPkg string 26 | ClientPkg string 27 | CLISchema string 28 | Dir string 29 | GoVersion string 30 | Overwrite bool 31 | PkgPath string 32 | ValidatorPkg string 33 | } 34 | 35 | type serverHints struct { 36 | Imports []string 37 | } 38 | 39 | type genctx struct { 40 | *parser.Result 41 | AppPkg string 42 | ClientPkg string 43 | CLISchema string 44 | Dir string 45 | GoVersion string 46 | Overwrite bool 47 | PkgPath string 48 | ServerHints serverHints 49 | ValidatorPkg string 50 | } 51 | 52 | type options struct { 53 | CLISchema string `long:"clischema"` 54 | } 55 | 56 | func Process(opts hsup.Options) error { 57 | var localopts options 58 | if _, err := flags.ParseArgs(&localopts, opts.Args); err != nil { 59 | return errors.Wrap(err, "failed to parse command line arguments") 60 | } 61 | 62 | b := New() 63 | b.Dir = opts.Dir 64 | b.AppPkg = opts.AppPkg 65 | b.GoVersion = opts.GoVersion 66 | b.PkgPath = opts.PkgPath 67 | b.Overwrite = opts.Overwrite 68 | b.CLISchema = localopts.CLISchema 69 | if err := b.ProcessFile(opts.Schema); err != nil { 70 | return err 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func New() *Builder { 77 | return &Builder{ 78 | ClientPkg: "client", 79 | Overwrite: false, 80 | ValidatorPkg: "validator", 81 | } 82 | } 83 | 84 | func (b *Builder) ProcessFile(f string) error { 85 | log.Printf(" ===> Using schema file '%s'", f) 86 | s, err := hschema.ReadFile(f) 87 | if err != nil { 88 | return errors.Wrap(err, "failed to read JSON Hyper Schema file") 89 | } 90 | return errors.Wrap(b.Process(s), "failed to process the JSON Hyper Schema") 91 | } 92 | 93 | func (b *Builder) Process(s *hschema.HyperSchema) error { 94 | if b.AppPkg == "" { 95 | return errors.New("AppPkg cannot be empty") 96 | } 97 | 98 | if b.PkgPath == "" { 99 | return errors.New("PkgPath cannot be empty") 100 | } 101 | 102 | ctx := genctx{ 103 | AppPkg: b.AppPkg, 104 | ClientPkg: b.ClientPkg, 105 | CLISchema: b.CLISchema, 106 | Dir: b.Dir, 107 | GoVersion: b.GoVersion, 108 | Overwrite: b.Overwrite, 109 | PkgPath: b.PkgPath, 110 | ValidatorPkg: b.ValidatorPkg, 111 | } 112 | 113 | if err := parse(&ctx, s); err != nil { 114 | return errors.Wrap(err, "failed to parse schema") 115 | } 116 | 117 | if err := generateFiles(&ctx); err != nil { 118 | return errors.Wrap(err, "failed to generate files") 119 | } 120 | 121 | log.Printf(" <=== All files generated") 122 | return nil 123 | } 124 | 125 | func parseServerHints(ctx *genctx, m map[string]interface{}) error { 126 | if v, ok := m["imports"]; ok { 127 | switch v.(type) { 128 | case []interface{}: 129 | default: 130 | return errors.New("invalid value type for imports: expected []interface{}") 131 | } 132 | 133 | l := v.([]interface{}) 134 | ctx.ServerHints.Imports = make([]string, len(l)) 135 | for i, n := range l { 136 | switch n.(type) { 137 | case string: 138 | default: 139 | return errors.New("invalid value type for elements in imports: expected string") 140 | } 141 | ctx.ServerHints.Imports[i] = n.(string) 142 | } 143 | } 144 | return nil 145 | } 146 | 147 | func parseExtras(ctx *genctx, s *hschema.HyperSchema) error { 148 | for k, v := range s.Extras { 149 | switch k { 150 | case "hsup.server": 151 | switch v.(type) { 152 | case map[string]interface{}: 153 | default: 154 | return errors.New("invalid value type for hsup.server: expected map[string]interface{}") 155 | } 156 | 157 | if err := parseServerHints(ctx, v.(map[string]interface{})); err != nil { 158 | return errors.Wrap(err, "failed to parse server hints") 159 | } 160 | } 161 | } 162 | return nil 163 | } 164 | 165 | func parse(ctx *genctx, s *hschema.HyperSchema) error { 166 | pres, err := parser.Parse(s) 167 | if err != nil { 168 | return errors.Wrap(err, "failed to parse schema") 169 | } 170 | ctx.Result = pres 171 | 172 | if err := parseExtras(ctx, s); err != nil { 173 | return errors.Wrap(err, "failed to parse extras") 174 | } 175 | 176 | for _, link := range s.Links { 177 | methodName := genutil.TitleToName(link.Title) 178 | methodBody, err := makeMethod(ctx, methodName, link) 179 | if err != nil { 180 | return errors.Wrap(err, "failed to make method '"+methodName+"'") 181 | } 182 | ctx.Methods[methodName] = methodBody 183 | if m := link.Extras; len(m) > 0 { 184 | w, ok := m[ext.WrapperKey] 185 | if ok { 186 | switch w.(type) { 187 | case string: 188 | ctx.MethodWrappers[methodName] = []string{w.(string)} 189 | case []interface{}: 190 | wl := w.([]interface{}) 191 | if len(wl) > 0 { 192 | rl := make([]string, len(wl)) 193 | for i, ws := range wl { 194 | switch ws.(type) { 195 | case string: 196 | rl[i] = ws.(string) 197 | default: 198 | return errors.New("wrapper elements must be strings") 199 | } 200 | } 201 | ctx.MethodWrappers[methodName] = rl 202 | } 203 | default: 204 | return errors.New("wrapper must be a string, or an array of strings") 205 | } 206 | } 207 | } 208 | } 209 | 210 | return nil 211 | } 212 | 213 | func makeMethod(ctx *genctx, name string, l *hschema.Link) (string, error) { 214 | buf := bytes.Buffer{} 215 | 216 | fmt.Fprintf(&buf, `func http%s(ctx context.Context, w http.ResponseWriter, r *http.Request) {`, name) 217 | buf.WriteString("\nif pdebug.Enabled {") 218 | fmt.Fprintf(&buf, "\ng := pdebug.Marker(%s)", strconv.Quote("http"+name)) 219 | buf.WriteString("\ndefer g.End()") 220 | buf.WriteString("\n}") 221 | 222 | method := strings.ToLower(l.Method) 223 | if method == "" { 224 | method = "get" 225 | } 226 | buf.WriteString("\nmethod := strings.ToLower(r.Method)") 227 | fmt.Fprintf(&buf, "\nif method != `%s` {", method) 228 | fmt.Fprintf(&buf, "\n"+`w.Header().Set("Allow", %s)`, strconv.Quote(method)) 229 | buf.WriteString("\nmsgbuf := getBytesBuffer()") 230 | buf.WriteString("\ndefer releaseBytesBuffer(msgbuf)") 231 | buf.WriteString("\nmsgbuf.WriteString(`Method was `)") 232 | buf.WriteString("\nmsgbuf.WriteString(r.Method)") 233 | buf.WriteString("\nmsgbuf.WriteString(`, expected '") 234 | buf.WriteString(method) 235 | buf.WriteString("'`)") 236 | buf.WriteString("\nhttpError(w, msgbuf.String(), http.StatusNotFound, nil)") 237 | buf.WriteString("\nreturn") 238 | buf.WriteString("\n}\n") 239 | 240 | if v, ok := ctx.RequestCORS[name]; ok { 241 | fmt.Fprintf(&buf, "\nw.Header().Set(`Access-Control-Allow-Origin`, %s)", strconv.Quote(v)) 242 | } 243 | 244 | payloadType := ctx.RequestPayloadType[name] 245 | 246 | if v := ctx.RequestValidators[name]; v != nil { 247 | // If this is a get request, then we'd have to assemble 248 | // the incoming data from r.Form 249 | if method == "get" { 250 | switch payloadType { 251 | case "interface{}", "map[string]interface{}": 252 | buf.WriteString("\nif err := r.ParseForm(); err != nil {") 253 | buf.WriteString("\nhttpError(w, `Failed to process query/post form`, http.StatusInternalServerError, nil)") 254 | buf.WriteString("\nreturn") 255 | buf.WriteString("\n}") 256 | buf.WriteString("\npayload := make(map[string]interface{})") 257 | 258 | pnames := make([]string, 0, len(l.Schema.Properties)) 259 | for k := range l.Schema.Properties { 260 | pnames = append(pnames, k) 261 | } 262 | sort.Strings(pnames) 263 | 264 | for _, k := range pnames { 265 | v := l.Schema.Properties[k] 266 | if !v.IsResolved() { 267 | rv, err := v.Resolve(ctx.Schema) 268 | if err != nil { 269 | return "", errors.Wrap(err, "failed to resolve schema") 270 | } 271 | v = rv 272 | } 273 | 274 | if len(v.Type) != 1 { 275 | return "", fmt.Errorf("'%s.%s' can't handle input parameters unless the type contains exactly 1 type (got: %v)", name, k, v.Type) 276 | } 277 | 278 | qk := strconv.Quote(k) 279 | buf.WriteString("\n{") 280 | switch v.Type[0] { 281 | case schema.IntegerType: 282 | fmt.Fprintf(&buf, "\nv, err := getInteger(r.Form, %s)", qk) 283 | fmt.Fprintf(&buf, ` 284 | if err != nil { 285 | msgbuf := getBytesBuffer() 286 | releaseBytesBuffer(msgbuf) 287 | msgbuf.WriteString("Invalid parameter %s") 288 | httpError(w, msgbuf.String(), http.StatusInternalServerError, err) 289 | return 290 | } 291 | `, k) 292 | case schema.StringType: 293 | fmt.Fprintf(&buf, "\nv := r.Form[%s]", qk) 294 | } 295 | fmt.Fprintf(&buf, ` 296 | switch len(v) { 297 | case 0: 298 | case 1: 299 | payload[%s] = v[0] 300 | default: 301 | payload[%s] = v 302 | } 303 | } 304 | `, qk, qk) 305 | } 306 | default: 307 | buf.WriteString("\nvar payload ") 308 | buf.WriteString(strings.TrimPrefix(payloadType, ctx.AppPkg+".")) 309 | buf.WriteString("\nqbuf := getBytesBuffer()") 310 | buf.WriteString("\ndefer releaseBytesBuffer(qbuf)") 311 | buf.WriteString("\nqbuf.WriteString(r.URL.RawQuery)") 312 | buf.WriteString("\nif err := urlenc.Unmarshal(qbuf.Bytes(), &payload); err != nil {") 313 | buf.WriteString("\nhttpError(w, `Failed to parse url query string`, http.StatusInternalServerError, err)") 314 | buf.WriteString("\nreturn") 315 | buf.WriteString("\n}") 316 | } 317 | } else { 318 | buf.WriteString("\nvar payload ") 319 | buf.WriteString(strings.TrimPrefix(payloadType, ctx.AppPkg+".")) 320 | 321 | buf.WriteString("\njsonbuf := getBytesBuffer()") 322 | buf.WriteString("\ndefer releaseBytesBuffer(jsonbuf)") 323 | buf.WriteString("\n\nswitch ct := r.Header.Get(\"Content-Type\"); {") 324 | buf.WriteString("\ncase ct == \"application/json\":") 325 | buf.WriteString("\nif _, err := io.Copy(jsonbuf, io.LimitReader(r.Body, MaxPostSize)); err != nil {") 326 | buf.WriteString("\nhttpError(w, `Failed to read request body`, http.StatusInternalServerError, err)") 327 | buf.WriteString("\nreturn") 328 | buf.WriteString("\n}") 329 | // If this is a multipart request, we must extract out the "payload" 330 | // field, and treat that as JSON 331 | if l.EncType == "multipart/form-data"{ 332 | buf.WriteString("\ncase strings.HasPrefix(ct, \"multipart/\"):") 333 | buf.WriteString("\nif err := r.ParseMultipartForm(MaxPostSize); err != nil {") 334 | buf.WriteString("\nhttpError(w, `Invalid multipart data`, http.StatusInternalServerError, err)") 335 | buf.WriteString("\nreturn") 336 | buf.WriteString("\n}") 337 | buf.WriteString("\nvals, ok := r.MultipartForm.Value[\"payload\"]") 338 | buf.WriteString("\nif ok && len(vals) > 0 {") 339 | buf.WriteString("\nif _, err := jsonbuf.WriteString(vals[0]); err != nil {") 340 | buf.WriteString("\nhttpError(w, `Failed to read payload`, http.StatusInternalServerError, err)") 341 | buf.WriteString("\nreturn") 342 | buf.WriteString("\n}") 343 | buf.WriteString("\n}") 344 | buf.WriteString("\npayload.MultipartForm = r.MultipartForm") 345 | } 346 | buf.WriteString("\ndefault:") 347 | buf.WriteString("\nhttpError(w, `Invalid content-type`, http.StatusInternalServerError, nil)") 348 | buf.WriteString("\nreturn") 349 | buf.WriteString("\n}") 350 | 351 | buf.WriteString("\nif pdebug.Enabled {") 352 | buf.WriteString("\npdebug.Printf(`-----> %s`, jsonbuf.Bytes())") 353 | buf.WriteString("\n}") 354 | buf.WriteString("\nif err := json.Unmarshal(jsonbuf.Bytes(), &payload); err != nil {") 355 | buf.WriteString("\nhttpError(w, `Invalid JSON input`, http.StatusInternalServerError, err)") 356 | buf.WriteString("\nreturn") 357 | buf.WriteString("\n}") 358 | } 359 | 360 | fmt.Fprintf(&buf, "\n\nif err := %s.%s.Validate(&payload); err != nil {", ctx.ValidatorPkg, v.Name) 361 | buf.WriteString("\nhttpError(w, `Invalid input (validation failed)`, http.StatusInternalServerError, err)") 362 | buf.WriteString("\nreturn") 363 | buf.WriteString("\n}") 364 | } 365 | 366 | fmt.Fprintf(&buf, "\ndo%s(ctx, w, r", name) 367 | if _, ok := ctx.RequestValidators[name]; ok { 368 | buf.WriteString(`, &payload`) 369 | } 370 | buf.WriteString(`)`) 371 | buf.WriteString("\n}\n") 372 | 373 | return buf.String(), nil 374 | } 375 | 376 | func generateFile(ctx *genctx, fn string, cb func(io.Writer, *genctx) error, forceOverwrite bool) error { 377 | if _, err := os.Stat(fn); err == nil { 378 | if !ctx.Overwrite { 379 | log.Printf(" - File '%s' already exists. Skipping", fn) 380 | return nil 381 | } 382 | if forceOverwrite { 383 | log.Printf(" * File '%s' already exists. Overwriting", fn) 384 | } else { 385 | log.Printf(" - File '%s' already exists. This file cannot be overwritten, skipping", fn) 386 | return nil 387 | } 388 | } 389 | 390 | log.Printf(" + Generating file '%s'", fn) 391 | f, err := genutil.CreateFile(fn) 392 | if err != nil { 393 | return errors.Wrap(err, "failed to generate file '"+fn+"'") 394 | } 395 | closed := false 396 | defer func() { 397 | if !closed { 398 | f.Close() 399 | } 400 | }() 401 | 402 | if err := cb(f, ctx); err != nil { 403 | f.Close() 404 | closed = true 405 | os.Remove(fn) 406 | return errors.Wrap(err, "callback failed") 407 | } 408 | return nil 409 | } 410 | 411 | func generateFiles(ctxif interface{}) error { 412 | ctx, ok := ctxif.(*genctx) 413 | if !ok { 414 | return errors.New("expected genctx type") 415 | } 416 | 417 | // these files are expected to be completely under control by the 418 | // hsup system, so get forcefully overwritten 419 | sysfiles := map[string]func(io.Writer, *genctx) error{ 420 | filepath.Join(ctx.Dir, fmt.Sprintf("%s_hsup.go", ctx.AppPkg)): generateServerCode, 421 | } 422 | for fn, cb := range sysfiles { 423 | if err := generateFile(ctx, fn, cb, true); err != nil { 424 | return errors.Wrap(err, "failed to generate file '"+fn+"'") 425 | } 426 | } 427 | 428 | // these files are expected to be modified by the author, so do 429 | // not get forcefully overwritten 430 | userfiles := map[string]func(io.Writer, *genctx) error{ 431 | filepath.Join(ctx.Dir, "cmd", ctx.AppPkg, fmt.Sprintf("%s.go", ctx.AppPkg)): generateExecutableCode, 432 | filepath.Join(ctx.Dir, "handlers.go"): generateStubHandlerCode, 433 | filepath.Join(ctx.Dir, "interface.go"): generateDataCode, 434 | filepath.Join(ctx.Dir, "client_test.go"): generateTestCode, 435 | } 436 | for fn, cb := range userfiles { 437 | if err := generateFile(ctx, fn, cb, false); err != nil { 438 | return errors.Wrap(err, "failed to generate file '"+fn+"'") 439 | } 440 | } 441 | 442 | return nil 443 | } 444 | 445 | func generateExecutableCode(out io.Writer, ctx *genctx) error { 446 | buf := bytes.Buffer{} 447 | buf.WriteString(`package main` + "\n\n") 448 | genutil.WriteImports( 449 | &buf, 450 | []string{"log", "os"}, 451 | []string{ctx.PkgPath, "github.com/jessevdk/go-flags", "github.com/pkg/errors"}, 452 | ) 453 | 454 | f := ctx.CLISchema 455 | if f == "" { 456 | buf.WriteString(`type options struct {` + "\n") 457 | buf.WriteString(`Listen string ` + "`" + `short:"l" long:"listen" default:":8080" description:"Listen address"` + "`\n") 458 | buf.WriteString("}\n") 459 | } else { 460 | s, err := schema.ReadFile(f) 461 | if err != nil { 462 | return errors.Wrap(err, "failed to read CLI schema file '"+f+"'") 463 | } 464 | 465 | buf.WriteString(`type options struct {` + "\n") 466 | for name, pschema := range s.Properties { 467 | var typ string 468 | typv, ok := pschema.Extras[ext.TypeKey] 469 | if !ok { 470 | switch pschema.Type[0] { 471 | case schema.IntegerType: 472 | typ = "int" 473 | case schema.StringType: 474 | typ = "string" 475 | case schema.BooleanType: 476 | typ = "bool" 477 | case schema.NumberType: 478 | typ = "float64" 479 | default: 480 | return errors.New("complex types cannot be automatically deduced. consider using 'hsup.type' key") 481 | } 482 | } else { 483 | typ, ok = typv.(string) 484 | if !ok { 485 | return errors.New("could not determine parameter type") 486 | } 487 | } 488 | buf.WriteString(genutil.TitleToName(name)) 489 | buf.WriteByte(' ') 490 | buf.WriteString(typ) 491 | buf.WriteByte(' ') 492 | buf.WriteString("`" + `long:"` + name + `"` + "`") 493 | } 494 | buf.WriteString("}\n") 495 | } 496 | 497 | buf.WriteString(` 498 | type ignorableError interface { 499 | Ignorable() bool 500 | } 501 | 502 | func isIgnorableError(err error) bool { 503 | if i, ok := err.(ignorableError); ok { 504 | return i.Ignorable() 505 | } 506 | return false 507 | } 508 | 509 | func main() { 510 | status := 1 511 | if err := _main(); err != nil { 512 | if !isIgnorableError(err) { 513 | println(err.Error()) 514 | } else { 515 | status = 0 516 | } 517 | } 518 | os.Exit(status) 519 | } 520 | 521 | func _main() error { 522 | var opts options 523 | if _, err := flags.Parse(&opts); err != nil { 524 | return errors.Wrap(err, "failed to parse command line options") 525 | } 526 | `) 527 | fmt.Fprintf(&buf, "if err := %s.ProcessOpts(&opts); err != nil {\n", ctx.AppPkg) 528 | buf.WriteString(`return errors.Wrap(err, "failed to process options") 529 | } 530 | 531 | log.Printf("Server listening on %s", opts.Listen) 532 | `) 533 | fmt.Fprintf(&buf, `if err := %s.Run(opts.Listen); err != nil {`+"\n", ctx.AppPkg) 534 | buf.WriteString(` log.Printf("%s", err) 535 | return 1 536 | } 537 | return 0 538 | }`) 539 | 540 | return genutil.WriteFmtCode(out, &buf) 541 | } 542 | 543 | func generateStubHandlerCode(out io.Writer, ctx *genctx) error { 544 | buf := bytes.Buffer{} 545 | 546 | fmt.Fprintf(&buf, "package %s\n\n", ctx.AppPkg) 547 | 548 | var extlibs []string 549 | if genutil.VersionCompare(ctx.GoVersion, "1.7") >= 0 { 550 | extlibs = append(extlibs, "context") 551 | } else { 552 | extlibs = append(extlibs, "golang.org/x/net/context") 553 | } 554 | genutil.WriteImports( 555 | &buf, 556 | []string{ 557 | "net/http", 558 | }, 559 | extlibs, 560 | ) 561 | 562 | for _, methodName := range ctx.MethodNames { 563 | payloadType := ctx.RequestPayloadType[methodName] 564 | payloadType = strings.TrimPrefix(payloadType, ctx.AppPkg+".") 565 | 566 | fmt.Fprintf(&buf, "\nfunc do%s(ctx context.Context, w http.ResponseWriter, r *http.Request", methodName) 567 | if _, ok := ctx.RequestValidators[methodName]; ok { 568 | buf.WriteString(`, payload *`) 569 | buf.WriteString(payloadType) 570 | } 571 | buf.WriteString(") {") 572 | buf.WriteString("\n}\n") 573 | } 574 | 575 | return genutil.WriteFmtCode(out, &buf) 576 | } 577 | 578 | func generateServerCode(out io.Writer, ctx *genctx) error { 579 | buf := bytes.Buffer{} 580 | 581 | fmt.Fprintf(&buf, "package %s\n\n", ctx.AppPkg) 582 | 583 | genutil.WriteDoNotEdit(&buf) 584 | 585 | imports := []string{ 586 | "io/ioutil", 587 | "github.com/gorilla/mux", 588 | "github.com/lestrrat-go/pdebug", 589 | "github.com/lestrrat-go/urlenc", 590 | } 591 | 592 | if genutil.VersionCompare(ctx.GoVersion, "1.7") >= 0 { 593 | imports = append(imports, "context") 594 | } else { 595 | imports = append(imports, "golang.org/x/net/context") 596 | } 597 | 598 | if len(ctx.RequestValidators) > 0 || len(ctx.ResponseValidators) > 0 { 599 | imports = append(imports, filepath.Join(ctx.PkgPath, "validator")) 600 | } 601 | 602 | if len(ctx.ServerHints.Imports) > 0 { 603 | imports = append(imports, ctx.ServerHints.Imports...) 604 | } 605 | 606 | genutil.WriteImports( 607 | &buf, 608 | []string{ 609 | "bytes", 610 | "encoding/json", 611 | "io", 612 | "net/http", 613 | "net/url", 614 | "strconv", 615 | "strings", 616 | "sync", 617 | }, 618 | imports, 619 | ) 620 | 621 | buf.WriteString(` 622 | const MaxPostSize = (1<<20)*2 623 | var _ = json.Decoder{} 624 | var _ = urlenc.Marshal 625 | var bbPool = sync.Pool{ 626 | New: allocBytesBuffer, 627 | } 628 | func allocBytesBuffer() interface{} { 629 | return &bytes.Buffer{} 630 | } 631 | 632 | func getBytesBuffer() *bytes.Buffer { 633 | return bbPool.Get().(*bytes.Buffer) 634 | } 635 | 636 | func releaseBytesBuffer(buf *bytes.Buffer) { 637 | buf.Reset() 638 | bbPool.Put(buf) 639 | } 640 | 641 | type Server struct { 642 | *mux.Router 643 | } 644 | 645 | // NewContext creates a cteonxt.Context object from the request. 646 | // If you are using appengine, for example, you probably want to set this 647 | // function to something that create a context, and then sets 648 | // the appengine context to it so it can be referred to later. 649 | var NewContext func(*http.Request) context.Context = func(r *http.Request) context.Context { 650 | return r.Context() 651 | } 652 | 653 | func Run(l string) error { 654 | s := New() 655 | return http.ListenAndServe(l, s.makeHandler()) 656 | } 657 | 658 | func New() *Server { 659 | s := &Server{ 660 | Router: mux.NewRouter(), 661 | } 662 | s.SetupRoutes() 663 | return s 664 | } 665 | 666 | var httpError func(http.ResponseWriter, string, int, error) = defaultHTTPError 667 | func defaultHTTPError(w http.ResponseWriter, message string, st int, err error) { 668 | if pdebug.Enabled { 669 | if err == nil { 670 | pdebug.Printf("HTTP Error %s", message) 671 | } else { 672 | pdebug.Printf("HTTP Error %s: %s", message, err) 673 | } 674 | } 675 | http.Error(w, http.StatusText(st), st) 676 | } 677 | 678 | func getInteger(v url.Values, f string) ([]int64, error) { 679 | x, ok := v[f] 680 | if !ok { 681 | return nil, nil 682 | } 683 | 684 | ret := make([]int64, len(x)) 685 | for i, e := range x { 686 | p, err := strconv.ParseInt(e, 10, 64) 687 | if err != nil { 688 | return nil, err 689 | } 690 | ret[i] = p 691 | } 692 | 693 | return ret, nil 694 | } 695 | 696 | type HandlerWithContext func(context.Context, http.ResponseWriter, *http.Request) 697 | func httpWithContext(h HandlerWithContext) http.HandlerFunc { 698 | return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { 699 | h(NewContext(r), w, r) 700 | defer io.Copy(ioutil.Discard, r.Body) 701 | }) 702 | } 703 | 704 | `) 705 | 706 | buf.WriteString("func (s *Server) makeHandler() http.Handler {\n") 707 | buf.WriteString("var h http.Handler\n") 708 | buf.WriteString("h = s\n") 709 | for _, middleware := range ctx.Middlewares { 710 | fmt.Fprintf(&buf, "h = %s.Wrap(h)\n", middleware) 711 | } 712 | buf.WriteString("return h\n") 713 | buf.WriteString("}\n\n") 714 | 715 | for _, methodName := range ctx.MethodNames { 716 | buf.WriteString(ctx.Methods[methodName]) 717 | buf.WriteString("\n") 718 | } 719 | 720 | buf.WriteString("func (s *Server) SetupRoutes() {") 721 | buf.WriteString("\nr := s.Router") 722 | 723 | paths := make([]string, 0, len(ctx.PathToMethods)) 724 | for path := range ctx.PathToMethods { 725 | paths = append(paths, path) 726 | } 727 | sort.Strings(paths) 728 | for _, path := range paths { 729 | method := ctx.PathToMethods[path] 730 | 731 | fmt.Fprintf(&buf, "\nr.HandleFunc(`%s`, httpWithContext(", path) 732 | for _, w := range ctx.MethodWrappers[method] { 733 | fmt.Fprintf(&buf, "%s(", w) 734 | } 735 | fmt.Fprintf(&buf, "http%s", method) 736 | for range ctx.MethodWrappers[method] { 737 | buf.WriteString(")") 738 | } 739 | buf.WriteString("))") 740 | } 741 | 742 | buf.WriteString("\n}\n") 743 | 744 | return genutil.WriteFmtCode(out, &buf) 745 | } 746 | 747 | func generateDataCode(out io.Writer, ctx *genctx) error { 748 | buf := bytes.Buffer{} 749 | fmt.Fprintf(&buf, `package %s`+"\n\n", ctx.AppPkg) 750 | 751 | types := make(map[string]struct{}) 752 | for _, t := range ctx.RequestPayloadType { 753 | types[t] = struct{}{} 754 | } 755 | for _, t := range ctx.ResponsePayloadType { 756 | types[t] = struct{}{} 757 | } 758 | 759 | for t := range types { 760 | if i := strings.IndexRune(t, '.'); i > -1 { // we have a qualified struct name? 761 | if prefix := t[:i+1]; prefix != "" { 762 | if prefix != ctx.AppPkg+"." { 763 | log.Printf(" * '%s' has a package name that's not the app package (%s != %s.)", t, prefix, ctx.AppPkg) 764 | continue 765 | } 766 | } 767 | t = strings.TrimPrefix(t, ctx.AppPkg+".") 768 | if genutil.LooksLikeStruct(t) { 769 | fmt.Fprintf(&buf, "type %s struct {}\n", t) 770 | } 771 | } 772 | } 773 | 774 | return genutil.WriteFmtCode(out, &buf) 775 | } 776 | 777 | func generateTestCode(out io.Writer, ctx *genctx) error { 778 | buf := bytes.Buffer{} 779 | 780 | fmt.Fprintf(&buf, "package %s_test\n\n", ctx.AppPkg) 781 | 782 | imports := []string{ 783 | ctx.PkgPath, 784 | filepath.Join(ctx.PkgPath, ctx.ClientPkg), 785 | "github.com/stretchr/testify/assert", 786 | } 787 | 788 | if len(ctx.ResponseValidators) > 0 { 789 | imports = append(imports, filepath.Join(ctx.PkgPath, ctx.ValidatorPkg)) 790 | } 791 | 792 | genutil.WriteImports( 793 | &buf, 794 | []string{ 795 | "testing", 796 | "net/http/httptest", 797 | }, 798 | imports, 799 | ) 800 | 801 | for _, methodName := range ctx.MethodNames { 802 | fmt.Fprintf(&buf, "func Test%s(t *testing.T) {\n", methodName) 803 | fmt.Fprintf(&buf, "ts := httptest.NewServer(%s.New())\n", ctx.AppPkg) 804 | buf.WriteString(`defer ts.Close() 805 | 806 | `) 807 | fmt.Fprintf(&buf, "cl := %s.New(ts.URL)\n", ctx.ClientPkg) 808 | 809 | if pt, ok := ctx.RequestPayloadType[methodName]; ok { 810 | buf.WriteString("var in ") 811 | if genutil.LooksLikeStruct(pt) { 812 | buf.WriteRune('*') 813 | } 814 | buf.WriteString(pt) 815 | buf.WriteString("\n") 816 | } 817 | if _, ok := ctx.ResponsePayloadType[methodName]; ok { 818 | buf.WriteString("res, ") 819 | } 820 | 821 | fmt.Fprintf(&buf, "err := cl.%s(", methodName) 822 | if _, ok := ctx.RequestPayloadType[methodName]; ok { 823 | buf.WriteString("in") 824 | } 825 | buf.WriteString(")\n") 826 | fmt.Fprintf(&buf, `if !assert.NoError(t, err, "%s should succeed") {`+"\n", methodName) 827 | buf.WriteString("return\n") 828 | buf.WriteString("}\n") 829 | if _, ok := ctx.ResponseValidators[methodName]; ok { 830 | fmt.Fprintf(&buf, `if !assert.NoError(t, %s.HTTP%sResponse.Validate(&res), "Validation should succeed") {`+"\n", ctx.ValidatorPkg, methodName) 831 | buf.WriteString("return\n}\n") 832 | } 833 | buf.WriteString("}\n\n") 834 | } 835 | 836 | return genutil.WriteFmtCode(out, &buf) 837 | } 838 | -------------------------------------------------------------------------------- /validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/jessevdk/go-flags" 12 | "github.com/lestrrat-go/hsup" 13 | "github.com/lestrrat-go/hsup/internal/genutil" 14 | "github.com/lestrrat-go/hsup/internal/parser" 15 | "github.com/lestrrat-go/jshschema" 16 | "github.com/lestrrat-go/jsval" 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | type Builder struct { 21 | AppPkg string 22 | Dir string 23 | Overwrite bool 24 | PkgPath string 25 | ValidatorPkg string 26 | } 27 | 28 | type genctx struct { 29 | *parser.Result 30 | AppPkg string 31 | Dir string 32 | Overwrite bool 33 | PkgPath string 34 | ValidatorPkg string 35 | } 36 | 37 | type options struct { 38 | } 39 | 40 | func Process(opts hsup.Options) error { 41 | var localopts options 42 | if _, err := flags.ParseArgs(&localopts, opts.Args); err != nil { 43 | return errors.Wrap(err, "failed to parse command line arguments") 44 | } 45 | 46 | b := New() 47 | b.Dir = opts.Dir 48 | b.AppPkg = opts.AppPkg 49 | b.PkgPath = opts.PkgPath 50 | b.Overwrite = opts.Overwrite 51 | if err := b.ProcessFile(opts.Schema); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func New() *Builder { 58 | return &Builder{ 59 | Overwrite: false, 60 | ValidatorPkg: "validator", 61 | } 62 | } 63 | 64 | func (b *Builder) ProcessFile(f string) error { 65 | log.Printf(" ===> Using schema file '%s'", f) 66 | s, err := hschema.ReadFile(f) 67 | if err != nil { 68 | return err 69 | } 70 | return b.Process(s) 71 | } 72 | 73 | func (b *Builder) Process(s *hschema.HyperSchema) error { 74 | if b.AppPkg == "" { 75 | return errors.New("AppPkg cannot be empty") 76 | } 77 | 78 | if b.PkgPath == "" { 79 | return errors.New("PkgPath cannot be empty") 80 | } 81 | 82 | ctx := genctx{ 83 | Dir: b.Dir, 84 | AppPkg: b.AppPkg, 85 | Overwrite: b.Overwrite, 86 | PkgPath: b.PkgPath, 87 | ValidatorPkg: b.ValidatorPkg, 88 | } 89 | 90 | if err := parse(&ctx, s); err != nil { 91 | return err 92 | } 93 | 94 | if err := generateFiles(&ctx); err != nil { 95 | return err 96 | } 97 | 98 | log.Printf(" <=== All files generated") 99 | return nil 100 | } 101 | 102 | func parse(ctx *genctx, s *hschema.HyperSchema) error { 103 | pres, err := parser.Parse(s) 104 | if err != nil { 105 | return err 106 | } 107 | ctx.Result = pres 108 | return nil 109 | } 110 | 111 | func generateFiles(ctx *genctx) error { 112 | { 113 | fn := filepath.Join(ctx.Dir, ctx.ValidatorPkg, fmt.Sprintf("%s.go", ctx.ValidatorPkg)) 114 | if err := generateFile(ctx, fn, generateValidatorCode); err != nil { 115 | return err 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func generateFile(ctx *genctx, fn string, cb func(io.Writer, *genctx) error) error { 123 | if _, err := os.Stat(fn); err == nil { 124 | if !ctx.Overwrite { 125 | log.Printf(" - File '%s' already exists. Skipping", fn) 126 | return nil 127 | } 128 | log.Printf(" * File '%s' already exists. Overwriting", fn) 129 | } 130 | 131 | log.Printf(" + Generating file '%s'", fn) 132 | f, err := genutil.CreateFile(fn) 133 | if err != nil { 134 | return err 135 | } 136 | defer f.Close() 137 | return cb(f, ctx) 138 | } 139 | 140 | func generateValidatorCode(out io.Writer, ctx *genctx) error { 141 | g := jsval.NewGenerator() 142 | validators := make([]*jsval.JSVal, 0, len(ctx.RequestValidators)+len(ctx.ResponseValidators)) 143 | for _, v := range ctx.RequestValidators { 144 | validators = append(validators, v) 145 | } 146 | for _, v := range ctx.ResponseValidators { 147 | validators = append(validators, v) 148 | } 149 | 150 | buf := bytes.Buffer{} 151 | genutil.WriteDoNotEdit(&buf) 152 | buf.WriteString("package " + ctx.ValidatorPkg + "\n\n") 153 | 154 | genutil.WriteImports( 155 | &buf, 156 | nil, 157 | []string{ 158 | "github.com/lestrrat-go/jsval", 159 | }, 160 | ) 161 | if err := g.Process(&buf, validators...); err != nil { 162 | return err 163 | } 164 | buf.WriteString("\n\n") 165 | 166 | return genutil.WriteFmtCode(out, &buf) 167 | } 168 | --------------------------------------------------------------------------------