github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/goagen/gen_js/generator.go (about) 1 package genjs 2 3 import ( 4 "flag" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 "text/template" 12 "time" 13 14 "github.com/goadesign/goa/design" 15 "github.com/goadesign/goa/goagen/codegen" 16 "github.com/goadesign/goa/goagen/utils" 17 ) 18 19 //NewGenerator returns an initialized instance of a JavaScript Client Generator 20 func NewGenerator(options ...Option) *Generator { 21 g := &Generator{} 22 23 for _, option := range options { 24 option(g) 25 } 26 27 return g 28 } 29 30 // Generator is the application code generator. 31 type Generator struct { 32 API *design.APIDefinition // The API definition 33 OutDir string // Destination directory 34 Timeout time.Duration // Timeout used by JavaScript client when making requests 35 Scheme string // Scheme used by JavaScript client 36 Host string // Host addressed by JavaScript client 37 NoExample bool // Do not generate an HTML example file 38 genfiles []string // Generated files 39 } 40 41 // Generate is the generator entry point called by the meta generator. 42 func Generate() (files []string, err error) { 43 var ( 44 outDir, ver string 45 timeout time.Duration 46 scheme, host string 47 noexample bool 48 ) 49 50 set := flag.NewFlagSet("client", flag.PanicOnError) 51 set.StringVar(&outDir, "out", "", "") 52 set.String("design", "", "") 53 set.DurationVar(&timeout, "timeout", time.Duration(20)*time.Second, "") 54 set.StringVar(&scheme, "scheme", "", "") 55 set.StringVar(&host, "host", "", "") 56 set.StringVar(&ver, "version", "", "") 57 set.BoolVar(&noexample, "noexample", false, "") 58 set.Parse(os.Args[1:]) 59 60 // First check compatibility 61 if err := codegen.CheckVersion(ver); err != nil { 62 return nil, err 63 } 64 65 // Now proceed 66 g := &Generator{OutDir: outDir, Timeout: timeout, Scheme: scheme, Host: host, NoExample: noexample, API: design.Design} 67 68 return g.Generate() 69 } 70 71 // Generate produces the skeleton main. 72 func (g *Generator) Generate() (_ []string, err error) { 73 if g.API == nil { 74 return nil, fmt.Errorf("missing API definition, make sure design is properly initialized") 75 } 76 77 go utils.Catch(nil, func() { g.Cleanup() }) 78 79 defer func() { 80 if err != nil { 81 g.Cleanup() 82 } 83 }() 84 85 if g.Timeout == 0 { 86 g.Timeout = 20 * time.Second 87 } 88 if g.Scheme == "" && len(g.API.Schemes) > 0 { 89 g.Scheme = g.API.Schemes[0] 90 } 91 if g.Scheme == "" { 92 g.Scheme = "http" 93 } 94 if g.Host == "" { 95 g.Host = g.API.Host 96 } 97 if g.Host == "" { 98 return nil, fmt.Errorf("missing host value, set it with --host") 99 } 100 101 g.OutDir = filepath.Join(g.OutDir, "js") 102 if err := os.RemoveAll(g.OutDir); err != nil { 103 return nil, err 104 } 105 if err := os.MkdirAll(g.OutDir, 0755); err != nil { 106 return nil, err 107 } 108 g.genfiles = append(g.genfiles, g.OutDir) 109 110 // Generate client.js 111 exampleAction, err := g.generateJS(filepath.Join(g.OutDir, "client.js")) 112 if err != nil { 113 return 114 } 115 116 // Generate axios.html 117 if err = g.generateAxiosJS(); err != nil { 118 return 119 } 120 121 if exampleAction != nil && !g.NoExample { 122 // Generate index.html 123 if err = g.generateIndexHTML(filepath.Join(g.OutDir, "index.html"), exampleAction); err != nil { 124 return 125 } 126 127 // Generate example 128 if err = g.generateExample(); err != nil { 129 return 130 } 131 } 132 133 return g.genfiles, nil 134 } 135 136 func (g *Generator) generateJS(jsFile string) (_ *design.ActionDefinition, err error) { 137 file, err := codegen.SourceFileFor(jsFile) 138 if err != nil { 139 return 140 } 141 g.genfiles = append(g.genfiles, jsFile) 142 143 data := map[string]interface{}{ 144 "API": g.API, 145 "Host": g.Host, 146 "Scheme": g.Scheme, 147 "Timeout": int64(g.Timeout / time.Millisecond), 148 } 149 if err = file.ExecuteTemplate("module", moduleT, nil, data); err != nil { 150 return 151 } 152 153 actions := make(map[string][]*design.ActionDefinition) 154 g.API.IterateResources(func(res *design.ResourceDefinition) error { 155 return res.IterateActions(func(action *design.ActionDefinition) error { 156 if as, ok := actions[action.Name]; ok { 157 actions[action.Name] = append(as, action) 158 } else { 159 actions[action.Name] = []*design.ActionDefinition{action} 160 } 161 return nil 162 }) 163 }) 164 165 var exampleAction *design.ActionDefinition 166 keys := []string{} 167 for n := range actions { 168 keys = append(keys, n) 169 } 170 sort.Strings(keys) 171 for _, n := range keys { 172 for _, a := range actions[n] { 173 if exampleAction == nil && a.Routes[0].Verb == "GET" { 174 exampleAction = a 175 } 176 data := map[string]interface{}{"Action": a} 177 funcs := template.FuncMap{"params": params} 178 if err = file.ExecuteTemplate("jsFuncs", jsFuncsT, funcs, data); err != nil { 179 return 180 } 181 } 182 } 183 184 _, err = file.Write([]byte(moduleTend)) 185 return exampleAction, err 186 } 187 188 func (g *Generator) generateIndexHTML(htmlFile string, exampleAction *design.ActionDefinition) error { 189 file, err := codegen.SourceFileFor(htmlFile) 190 if err != nil { 191 return err 192 } 193 g.genfiles = append(g.genfiles, htmlFile) 194 195 argNames := params(exampleAction) 196 var args string 197 if len(argNames) > 0 { 198 query := exampleAction.QueryParams.Type.ToObject() 199 argValues := make([]string, len(argNames)) 200 for i, n := range argNames { 201 ex := query[n].GenerateExample(g.API.RandomGenerator(), nil) 202 argValues[i] = fmt.Sprintf("%v", ex) 203 } 204 args = strings.Join(argValues, ", ") 205 } 206 examplePath := exampleAction.Routes[0].FullPath() 207 pathParams := exampleAction.Routes[0].Params() 208 if len(pathParams) > 0 { 209 pathVars := exampleAction.AllParams().Type.ToObject() 210 pathValues := make([]interface{}, len(pathParams)) 211 for i, n := range pathParams { 212 ex := pathVars[n].GenerateExample(g.API.RandomGenerator(), nil) 213 pathValues[i] = ex 214 } 215 format := design.WildcardRegex.ReplaceAllLiteralString(examplePath, "/%v") 216 examplePath = fmt.Sprintf(format, pathValues...) 217 } 218 if len(argNames) > 0 { 219 args = ", " + args 220 } 221 exampleFunc := fmt.Sprintf( 222 `%s%s ("%s"%s)`, 223 exampleAction.Name, 224 strings.Title(exampleAction.Parent.Name), 225 examplePath, 226 args, 227 ) 228 data := map[string]interface{}{ 229 "API": g.API, 230 "ExampleFunc": exampleFunc, 231 } 232 233 return file.ExecuteTemplate("exampleHTML", exampleT, nil, data) 234 } 235 236 func (g *Generator) generateAxiosJS() error { 237 filePath := filepath.Join(g.OutDir, "axios.min.js") 238 if err := ioutil.WriteFile(filePath, []byte(axios), 0644); err != nil { 239 return err 240 } 241 g.genfiles = append(g.genfiles, filePath) 242 243 return nil 244 } 245 246 func (g *Generator) generateExample() error { 247 controllerFile := filepath.Join(g.OutDir, "example.go") 248 file, err := codegen.SourceFileFor(controllerFile) 249 if err != nil { 250 return err 251 } 252 imports := []*codegen.ImportSpec{ 253 codegen.SimpleImport("net/http"), 254 codegen.SimpleImport("github.com/dimfeld/httptreemux"), 255 codegen.SimpleImport("github.com/goadesign/goa"), 256 } 257 if err := file.WriteHeader(fmt.Sprintf("%s JavaScript Client Example", g.API.Name), "js", imports); err != nil { 258 return err 259 } 260 g.genfiles = append(g.genfiles, controllerFile) 261 262 data := map[string]interface{}{"ServeDir": g.OutDir} 263 if err := file.ExecuteTemplate("examples", exampleCtrlT, nil, data); err != nil { 264 return err 265 } 266 267 return file.FormatCode() 268 } 269 270 // Cleanup removes all the files generated by this generator during the last invokation of Generate. 271 func (g *Generator) Cleanup() { 272 for _, f := range g.genfiles { 273 os.Remove(f) 274 } 275 g.genfiles = nil 276 } 277 278 func params(action *design.ActionDefinition) []string { 279 if action.QueryParams == nil { 280 return nil 281 } 282 params := make([]string, len(action.QueryParams.Type.ToObject())) 283 i := 0 284 for n := range action.QueryParams.Type.ToObject() { 285 params[i] = n 286 i++ 287 } 288 sort.Strings(params) 289 return params 290 } 291 292 const moduleT = `// This module exports functions that give access to the {{.API.Name}} API hosted at {{.API.Host}}. 293 // It uses the axios javascript library for making the actual HTTP requests. 294 define(['axios'] , function (axios) { 295 function merge(obj1, obj2) { 296 var obj3 = {}; 297 for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; } 298 for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; } 299 return obj3; 300 } 301 302 return function (scheme, host, timeout) { 303 scheme = scheme || '{{.Scheme}}'; 304 host = host || '{{.Host}}'; 305 timeout = timeout || {{.Timeout}}; 306 307 // Client is the object returned by this module. 308 var client = axios; 309 310 // URL prefix for all API requests. 311 var urlPrefix = scheme + '://' + host; 312 ` 313 314 const moduleTend = ` return client; 315 }; 316 }); 317 ` 318 319 const jsFuncsT = `{{$params := params .Action}} 320 {{$name := printf "%s%s" .Action.Name (title .Action.Parent.Name)}}// {{if .Action.Description}}{{.Action.Description}}{{else}}{{$name}} calls the {{.Action.Name}} action of the {{.Action.Parent.Name}} resource.{{end}} 321 // path is the request path, the format is "{{(index .Action.Routes 0).FullPath}}" 322 {{if .Action.Payload}}// data contains the action payload (request body) 323 {{end}}{{if $params}}// {{join $params ", "}} {{if gt (len $params) 1}}are{{else}}is{{end}} used to build the request query string. 324 {{end}}// config is an optional object to be merged into the config built by the function prior to making the request. 325 // The content of the config object is described here: https://github.com/mzabriskie/axios#request-api 326 // This function returns a promise which raises an error if the HTTP response is a 4xx or 5xx. 327 client.{{$name}} = function (path{{if .Action.Payload}}, data{{end}}{{if $params}}, {{join $params ", "}}{{end}}, config) { 328 cfg = { 329 timeout: timeout, 330 url: urlPrefix + path, 331 method: '{{toLower (index .Action.Routes 0).Verb}}', 332 {{if $params}} params: { 333 {{range $index, $param := $params}}{{if $index}}, 334 {{end}} {{$param}}: {{$param}}{{end}} 335 }, 336 {{end}}{{if .Action.Payload}} data: data, 337 {{end}} responseType: 'json' 338 }; 339 if (config) { 340 cfg = merge(cfg, config); 341 } 342 return client(cfg); 343 } 344 ` 345 346 const exampleT = `<!doctype html> 347 <html> 348 <head> 349 <title>goa JavaScript client loader</title> 350 </head> 351 <body> 352 <h1>{{.API.Name}} Client Test</h1> 353 <div id="response"></div> 354 <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.16/require.min.js"></script> 355 <script> 356 requirejs.config({ 357 paths: { 358 axios: '/js/axios.min', 359 client: '/js/client' 360 } 361 }); 362 requirejs(['client'], function (client) { 363 client().{{.ExampleFunc}} 364 .then(function (resp) { 365 document.getElementById('response').innerHTML = resp.statusText; 366 }) 367 .catch(function (resp) { 368 document.getElementById('response').innerHTML = resp.statusText; 369 }); 370 }); 371 </script> 372 </body> 373 </html> 374 ` 375 376 const exampleCtrlT = `// MountController mounts the JavaScript example controller under "/js". 377 // This is just an example, not the best way to do this. A better way would be to specify a file 378 // server using the Files DSL in the design. 379 // Use --noexample to prevent this file from being generated. 380 func MountController(service *goa.Service) { 381 // Serve static files under js 382 service.ServeFiles("/js/*filepath", {{printf "%q" .ServeDir}}) 383 service.LogInfo("mount", "ctrl", "JS", "action", "ServeFiles", "route", "GET /js/*") 384 } 385 ` 386 387 const axios = `/* axios v0.7.0 | (c) 2015 by Matt Zabriskie */ 388 !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";var r=n(2),o=n(3),i=n(4),s=n(12),u=e.exports=function(e){"string"==typeof e&&(e=o.merge({url:arguments[0]},arguments[1])),e=o.merge({method:"get",headers:{},timeout:r.timeout,transformRequest:r.transformRequest,transformResponse:r.transformResponse},e),e.withCredentials=e.withCredentials||r.withCredentials;var t=[i,void 0],n=Promise.resolve(e);for(u.interceptors.request.forEach(function(e){t.unshift(e.fulfilled,e.rejected)}),u.interceptors.response.forEach(function(e){t.push(e.fulfilled,e.rejected)});t.length;)n=n.then(t.shift(),t.shift());return n};u.defaults=r,u.all=function(e){return Promise.all(e)},u.spread=n(13),u.interceptors={request:new s,response:new s},function(){function e(){o.forEach(arguments,function(e){u[e]=function(t,n){return u(o.merge(n||{},{method:e,url:t}))}})}function t(){o.forEach(arguments,function(e){u[e]=function(t,n,r){return u(o.merge(r||{},{method:e,url:t,data:n}))}})}e("delete","get","head"),t("post","put","patch")}()},function(e,t,n){"use strict";var r=n(3),o=/^\)\]\}',?\n/,i={"Content-Type":"application/x-www-form-urlencoded"};e.exports={transformRequest:[function(e,t){return r.isFormData(e)?e:r.isArrayBuffer(e)?e:r.isArrayBufferView(e)?e.buffer:!r.isObject(e)||r.isFile(e)||r.isBlob(e)?e:(r.isUndefined(t)||(r.forEach(t,function(e,n){"content-type"===n.toLowerCase()&&(t["Content-Type"]=e)}),r.isUndefined(t["Content-Type"])&&(t["Content-Type"]="application/json")),JSON.stringify(e))}],transformResponse:[function(e){if("string"==typeof e){e=e.replace(o,"");try{e=JSON.parse(e)}catch(t){}}return e}],headers:{common:{Accept:"application/json, text/plain, */*"},patch:r.merge(i),post:r.merge(i),put:r.merge(i)},timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"}},function(e,t){"use strict";function n(e){return"[object Array]"===v.call(e)}function r(e){return"[object ArrayBuffer]"===v.call(e)}function o(e){return"[object FormData]"===v.call(e)}function i(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function s(e){return"string"==typeof e}function u(e){return"number"==typeof e}function a(e){return"undefined"==typeof e}function f(e){return null!==e&&"object"==typeof e}function c(e){return"[object Date]"===v.call(e)}function p(e){return"[object File]"===v.call(e)}function l(e){return"[object Blob]"===v.call(e)}function d(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function h(e){return"[object Arguments]"===v.call(e)}function m(){return"undefined"!=typeof window&&"undefined"!=typeof document&&"function"==typeof document.createElement}function y(e,t){if(null!==e&&"undefined"!=typeof e){var r=n(e)||h(e);if("object"==typeof e||r||(e=[e]),r)for(var o=0,i=e.length;i>o;o++)t.call(null,e[o],o,e);else for(var s in e)e.hasOwnProperty(s)&&t.call(null,e[s],s,e)}}function g(){var e={};return y(arguments,function(t){y(t,function(t,n){e[n]=t})}),e}var v=Object.prototype.toString;e.exports={isArray:n,isArrayBuffer:r,isFormData:o,isArrayBufferView:i,isString:s,isNumber:u,isObject:f,isUndefined:a,isDate:c,isFile:p,isBlob:l,isStandardBrowserEnv:m,forEach:y,merge:g,trim:d}},function(e,t,n){(function(t){"use strict";e.exports=function(e){return new Promise(function(r,o){try{"undefined"!=typeof XMLHttpRequest||"undefined"!=typeof ActiveXObject?n(6)(r,o,e):"undefined"!=typeof t&&n(6)(r,o,e)}catch(i){o(i)}})}}).call(t,n(5))},function(e,t){function n(){f=!1,s.length?a=s.concat(a):c=-1,a.length&&r()}function r(){if(!f){var e=setTimeout(n);f=!0;for(var t=a.length;t;){for(s=a,a=[];++c<t;)s&&s[c].run();c=-1,t=a.length}s=null,f=!1,clearTimeout(e)}}function o(e,t){this.fun=e,this.array=t}function i(){}var s,u=e.exports={},a=[],f=!1,c=-1;u.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];a.push(new o(e,t)),1!==a.length||f||setTimeout(r,0)},o.prototype.run=function(){this.fun.apply(null,this.array)},u.title="browser",u.browser=!0,u.env={},u.argv=[],u.version="",u.versions={},u.on=i,u.addListener=i,u.once=i,u.off=i,u.removeListener=i,u.removeAllListeners=i,u.emit=i,u.binding=function(e){throw new Error("process.binding is not supported")},u.cwd=function(){return"/"},u.chdir=function(e){throw new Error("process.chdir is not supported")},u.umask=function(){return 0}},function(e,t,n){"use strict";var r=n(2),o=n(3),i=n(7),s=n(8),u=n(9);e.exports=function(e,t,a){var f=u(a.data,a.headers,a.transformRequest),c=o.merge(r.headers.common,r.headers[a.method]||{},a.headers||{});o.isFormData(f)&&delete c["Content-Type"];var p=new(XMLHttpRequest||ActiveXObject)("Microsoft.XMLHTTP");if(p.open(a.method.toUpperCase(),i(a.url,a.params),!0),p.timeout=a.timeout,p.onreadystatechange=function(){if(p&&4===p.readyState){var n=s(p.getAllResponseHeaders()),r=-1!==["text",""].indexOf(a.responseType||"")?p.responseText:p.response,o={data:u(r,n,a.transformResponse),status:p.status,statusText:p.statusText,headers:n,config:a};(p.status>=200&&p.status<300?e:t)(o),p=null}},o.isStandardBrowserEnv()){var l=n(10),d=n(11),h=d(a.url)?l.read(a.xsrfCookieName||r.xsrfCookieName):void 0;h&&(c[a.xsrfHeaderName||r.xsrfHeaderName]=h)}if(o.forEach(c,function(e,t){f||"content-type"!==t.toLowerCase()?p.setRequestHeader(t,e):delete c[t]}),a.withCredentials&&(p.withCredentials=!0),a.responseType)try{p.responseType=a.responseType}catch(m){if("json"!==p.responseType)throw m}o.isArrayBuffer(f)&&(f=new DataView(f)),p.send(f)}},function(e,t,n){"use strict";function r(e){return encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}var o=n(3);e.exports=function(e,t){if(!t)return e;var n=[];return o.forEach(t,function(e,t){null!==e&&"undefined"!=typeof e&&(o.isArray(e)&&(t+="[]"),o.isArray(e)||(e=[e]),o.forEach(e,function(e){o.isDate(e)?e=e.toISOString():o.isObject(e)&&(e=JSON.stringify(e)),n.push(r(t)+"="+r(e))}))}),n.length>0&&(e+=(-1===e.indexOf("?")?"?":"&")+n.join("&")),e}},function(e,t,n){"use strict";var r=n(3);e.exports=function(e){var t,n,o,i={};return e?(r.forEach(e.split("\n"),function(e){o=e.indexOf(":"),t=r.trim(e.substr(0,o)).toLowerCase(),n=r.trim(e.substr(o+1)),t&&(i[t]=i[t]?i[t]+", "+n:n)}),i):i}},function(e,t,n){"use strict";var r=n(3);e.exports=function(e,t,n){return r.forEach(n,function(n){e=n(e,t)}),e}},function(e,t,n){"use strict";var r=n(3);e.exports={write:function(e,t,n,o,i,s){var u=[];u.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&u.push("expires="+new Date(n).toGMTString()),r.isString(o)&&u.push("path="+o),r.isString(i)&&u.push("domain="+i),s===!0&&u.push("secure"),document.cookie=u.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}},function(e,t,n){"use strict";function r(e){var t=e;return s&&(u.setAttribute("href",t),t=u.href),u.setAttribute("href",t),{href:u.href,protocol:u.protocol?u.protocol.replace(/:$/,""):"",host:u.host,search:u.search?u.search.replace(/^\?/,""):"",hash:u.hash?u.hash.replace(/^#/,""):"",hostname:u.hostname,port:u.port,pathname:"/"===u.pathname.charAt(0)?u.pathname:"/"+u.pathname}}var o,i=n(3),s=/(msie|trident)/i.test(navigator.userAgent),u=document.createElement("a");o=r(window.location.href),e.exports=function(e){var t=i.isString(e)?r(e):e;return t.protocol===o.protocol&&t.host===o.host}},function(e,t,n){"use strict";function r(){this.handlers=[]}var o=n(3);r.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},r.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},r.prototype.forEach=function(e){o.forEach(this.handlers,function(t){null!==t&&e(t)})},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])});`