diff --git a/examples/browser/.gitignore b/examples/browser/.gitignore new file mode 100644 index 00000000000..75ce18e9269 --- /dev/null +++ b/examples/browser/.gitignore @@ -0,0 +1,2 @@ +/bower_components +/node_modules diff --git a/examples/browser/README.md b/examples/browser/README.md new file mode 100644 index 00000000000..be4fbd31141 --- /dev/null +++ b/examples/browser/README.md @@ -0,0 +1,31 @@ +# Browser example + +This directory contains an example use of grpc-gateway with web browsers. +The following commands automatically runs integration tests with phantomjs. + +```shell-session +$ npm install -g gulp-cli +$ npm install +$ gulp +``` + +## Other examples + +### Very simple example +Run +```shell-session +$ gulp bower +$ gulp backends +``` + +then, open `index.html`. + + +### Integration test with your browser + +Run +```shell-session +$ gulp serve +``` + +then, open `http://localhost:8000` with your browser. diff --git a/examples/browser/a_bit_of_everything_service.spec.js b/examples/browser/a_bit_of_everything_service.spec.js new file mode 100644 index 00000000000..edcbebe11d6 --- /dev/null +++ b/examples/browser/a_bit_of_everything_service.spec.js @@ -0,0 +1,185 @@ +'use strict'; + +var SwaggerClient = require('swagger-client'); + +describe('ABitOfEverythingService', function() { + var client; + + beforeEach(function(done) { + new SwaggerClient({ + url: "http://localhost:8080/swagger/a_bit_of_everything.swagger.json", + usePromise: true, + }).then(function(c) { + client = c; + }).catch(function(err) { + done.fail(err); + }).then(done); + }); + + describe('Create', function() { + var created; + var expected = { + float_value: 1.5, + double_value: 2.5, + int64_value: "4294967296", + uint64_value: "9223372036854775807", + int32_value: -2147483648, + fixed64_value: "9223372036854775807", + fixed32_value: 4294967295, + bool_value: true, + string_value: "strprefix/foo", + uint32_value: 4294967295, + sfixed32_value: 2147483647, + sfixed64_value: "-4611686018427387904", + sint32_value: 2147483647, + sint64_value: "4611686018427387903", + nonConventionalNameValue: "camelCase", + }; + + beforeEach(function(done) { + client.ABitOfEverythingService.Create(expected).then(function(resp) { + created = resp.obj; + }).catch(function(err) { + done.fail(err); + }).then(done); + }); + + it('should assign id', function() { + expect(created.uuid).not.toBe(""); + }); + + it('should echo the request back', function() { + delete created.uuid; + expect(created).toEqual(expected); + }); + }); + + describe('CreateBody', function() { + var created; + var expected = { + float_value: 1.5, + double_value: 2.5, + int64_value: "4294967296", + uint64_value: "9223372036854775807", + int32_value: -2147483648, + fixed64_value: "9223372036854775807", + fixed32_value: 4294967295, + bool_value: true, + string_value: "strprefix/foo", + uint32_value: 4294967295, + sfixed32_value: 2147483647, + sfixed64_value: "-4611686018427387904", + sint32_value: 2147483647, + sint64_value: "4611686018427387903", + nonConventionalNameValue: "camelCase", + + nested: [ + { name: "bar", amount: 10 }, + { name: "baz", amount: 20 }, + ], + repeated_string_value: ["a", "b", "c"], + oneof_string: "x", + // TODO(yugui) Support enum by name + map_value: { a: 1, b: 2 }, + mapped_string_value: { a: "x", b: "y" }, + mapped_nested_value: { + a: { name: "x", amount: 1 }, + b: { name: "y", amount: 2 }, + }, + }; + + beforeEach(function(done) { + client.ABitOfEverythingService.CreateBody({ + body: expected, + }).then(function(resp) { + created = resp.obj; + }).catch(function(err) { + done.fail(err); + }).then(done); + }); + + it('should assign id', function() { + expect(created.uuid).not.toBe(""); + }); + + it('should echo the request back', function() { + delete created.uuid; + expect(created).toEqual(expected); + }); + }); + + describe('lookup', function() { + var created; + var expected = { + bool_value: true, + string_value: "strprefix/foo", + }; + + beforeEach(function(done) { + client.ABitOfEverythingService.CreateBody({ + body: expected, + }).then(function(resp) { + created = resp.obj; + }).catch(function(err) { + fail(err); + }).finally(done); + }); + + it('should look up an object by uuid', function(done) { + client.ABitOfEverythingService.Lookup({ + uuid: created.uuid + }).then(function(resp) { + expect(resp.obj).toEqual(created); + }).catch(function(err) { + fail(err.errObj); + }).finally(done); + }); + + it('should fail if no such object', function(done) { + client.ABitOfEverythingService.Lookup({ + uuid: 'not_exist', + }).then(function(resp) { + fail('expected failure but succeeded'); + }).catch(function(err) { + expect(err.status).toBe(404); + }).finally(done); + }); + }); + + describe('Delete', function() { + var created; + var expected = { + bool_value: true, + string_value: "strprefix/foo", + }; + + beforeEach(function(done) { + client.ABitOfEverythingService.CreateBody({ + body: expected, + }).then(function(resp) { + created = resp.obj; + }).catch(function(err) { + fail(err); + }).finally(done); + }); + + it('should delete an object by id', function(done) { + client.ABitOfEverythingService.Delete({ + uuid: created.uuid + }).then(function(resp) { + expect(resp.obj).toEqual({}); + }).catch(function(err) { + fail(err.errObj); + }).then(function() { + return client.ABitOfEverythingService.Lookup({ + uuid: created.uuid + }); + }).then(function(resp) { + fail('expected failure but succeeded'); + }). catch(function(err) { + expect(err.status).toBe(404); + }).finally(done); + }); + }); +}); + diff --git a/examples/browser/bin/.gitignore b/examples/browser/bin/.gitignore new file mode 100644 index 00000000000..a68d087bfe5 --- /dev/null +++ b/examples/browser/bin/.gitignore @@ -0,0 +1,2 @@ +/* +!/.gitignore diff --git a/examples/browser/bower.json b/examples/browser/bower.json new file mode 100644 index 00000000000..eb2c258d6f2 --- /dev/null +++ b/examples/browser/bower.json @@ -0,0 +1,21 @@ +{ + "name": "grpc-gateway-example-browser", + "description": "Example use of grpc-gateway from browser", + "main": "index.js", + "authors": [ + "Yuki Yugui Sonoda " + ], + "license": "SEE LICENSE IN LICENSE file", + "homepage": "https://github.com/gengo/grpc-gateway", + "private": true, + "dependencies": { + "swagger-js": "~> 2.1" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/examples/browser/echo_service.spec.js b/examples/browser/echo_service.spec.js new file mode 100644 index 00000000000..97888c3e6c7 --- /dev/null +++ b/examples/browser/echo_service.spec.js @@ -0,0 +1,43 @@ +'use strict'; + +var SwaggerClient = require('swagger-client'); + +describe('EchoService', function() { + var client; + + beforeEach(function(done) { + new SwaggerClient({ + url: "http://localhost:8080/swagger/echo_service.swagger.json", + usePromise: true, + }).then(function(c) { + client = c; + done(); + }); + }); + + describe('Echo', function() { + it('should echo the request back', function(done) { + client.EchoService.Echo( + {id: "foo"}, + {responseContentType: "application/json"} + ).then(function(resp) { + expect(resp.obj).toEqual({id: "foo"}); + }).catch(function(err) { + done.fail(err); + }).then(done); + }); + }); + + describe('EchoBody', function() { + it('should echo the request back', function(done) { + client.EchoService.EchoBody( + {body: {id: "foo"}}, + {responseContentType: "application/json"} + ).then(function(resp) { + expect(resp.obj).toEqual({id: "foo"}); + }).catch(function(err) { + done.fail(err); + }).then(done); + }); + }); +}); diff --git a/examples/browser/gulpfile.js b/examples/browser/gulpfile.js new file mode 100644 index 00000000000..c095a3e6cbb --- /dev/null +++ b/examples/browser/gulpfile.js @@ -0,0 +1,81 @@ +"use strict"; + +var gulp = require('gulp'); + +var path = require('path'); + +var bower = require('gulp-bower'); +var exit = require('gulp-exit'); +var gprocess = require('gulp-process'); +var shell = require('gulp-shell'); +var jasmineBrowser = require('gulp-jasmine-browser'); +var webpack = require('webpack-stream'); + +gulp.task('bower', function(){ + return bower(); +}); + +gulp.task('server', shell.task([ + 'go build -o bin/example-server github.com/gengo/grpc-gateway/examples/server', +])); + +gulp.task('gateway', shell.task([ + 'go build -o bin/example-gw github.com/gengo/grpc-gateway/examples', +])); + +gulp.task('serve-server', ['server'], function(){ + gprocess.start('server-server', 'bin/example-server', [ + '--logtostderr', + ]); + gulp.watch('bin/example-server', ['serve-server']); +}); + +gulp.task('serve-gateway', ['gateway', 'serve-server'], function(){ + gprocess.start('gateway-server', 'bin/example-gw', [ + '--logtostderr', '--swagger_dir', path.join(__dirname, "../examplepb"), + ]); + gulp.watch('bin/example-gateway', ['serve-gateway']); +}); + +gulp.task('backends', ['serve-gateway', 'serve-server']); + +var specFiles = ['*.spec.js']; +gulp.task('test', ['backends'], function(done) { + return gulp.src(specFiles) + .pipe(webpack({output: {filename: 'spec.js'}})) + .pipe(jasmineBrowser.specRunner({ + console: true, + sourceMappedStacktrace: true, + })) + .pipe(jasmineBrowser.headless({ + findOpenPort: true, + catch: true, + throwFailures: true, + })) + .on('error', function(err) { + done(err); + process.exit(1); + }) + .pipe(exit()); +}); + +gulp.task('serve', ['backends'], function(done) { + var JasminePlugin = require('gulp-jasmine-browser/webpack/jasmine-plugin'); + var plugin = new JasminePlugin(); + + return gulp.src(specFiles) + .pipe(webpack({ + output: {filename: 'spec.js'}, + watch: true, + plugins: [plugin], + })) + .pipe(jasmineBrowser.specRunner({ + sourceMappedStacktrace: true, + })) + .pipe(jasmineBrowser.server({ + port: 8000, + whenReady: plugin.whenReady, + })); +}); + +gulp.task('default', ['test']); diff --git a/examples/browser/index.html b/examples/browser/index.html new file mode 100644 index 00000000000..7817451ca82 --- /dev/null +++ b/examples/browser/index.html @@ -0,0 +1,22 @@ + + + + + + + +
+ + diff --git a/examples/browser/package.json b/examples/browser/package.json new file mode 100644 index 00000000000..c78bdcbf0c9 --- /dev/null +++ b/examples/browser/package.json @@ -0,0 +1,23 @@ +{ + "name": "grpc-gateway-example", + "version": "1.0.0", + "description": "Example use of grpc-gateway from browser", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "SEE LICENSE IN LICENSE.txt", + "devDependencies": { + "bower": "^1.7.9", + "gulp": "^3.9.1", + "gulp-bower": "0.0.13", + "gulp-exit": "0.0.2", + "gulp-jasmine-browser": "^1.3.2", + "gulp-process": "^0.1.2", + "gulp-shell": "^0.5.2", + "jasmine": "^2.4.1", + "phantomjs": "^2.1.7", + "swagger-client": "^2.1.14", + "webpack-stream": "^3.2.0" + } +} diff --git a/examples/main.go b/examples/main.go index f9111c9d93b..2d8a1a54aff 100644 --- a/examples/main.go +++ b/examples/main.go @@ -3,6 +3,8 @@ package main import ( "flag" "net/http" + "path" + "strings" "github.com/gengo/grpc-gateway/examples/examplepb" "github.com/gengo/grpc-gateway/runtime" @@ -15,33 +17,86 @@ var ( echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of EchoService") abeEndpoint = flag.String("more_endpoint", "localhost:9090", "endpoint of ABitOfEverythingService") flowEndpoint = flag.String("flow_endpoint", "localhost:9090", "endpoint of FlowCombination") -) -func Run(address string, opts ...runtime.ServeMuxOption) error { - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() + swaggerDir = flag.String("swagger_dir", "examples/examplepb", "path to the directory which contains swagger definitions") +) +// newGateway returns a new gateway server which translates HTTP into gRPC. +func newGateway(ctx context.Context, opts ...runtime.ServeMuxOption) (http.Handler, error) { mux := runtime.NewServeMux(opts...) dialOpts := []grpc.DialOption{grpc.WithInsecure()} err := examplepb.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, dialOpts) if err != nil { - return err + return nil, err } err = examplepb.RegisterStreamServiceHandlerFromEndpoint(ctx, mux, *abeEndpoint, dialOpts) if err != nil { - return err + return nil, err } err = examplepb.RegisterABitOfEverythingServiceHandlerFromEndpoint(ctx, mux, *abeEndpoint, dialOpts) if err != nil { - return err + return nil, err } err = examplepb.RegisterFlowCombinationHandlerFromEndpoint(ctx, mux, *flowEndpoint, dialOpts) + if err != nil { + return nil, err + } + return mux, nil +} + +func serveSwagger(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, ".swagger.json") { + glog.Errorf("Not Found: %s", r.URL.Path) + http.NotFound(w, r) + return + } + + glog.Infof("Serving %s", r.URL.Path) + p := strings.TrimPrefix(r.URL.Path, "/swagger/") + p = path.Join(*swaggerDir, p) + http.ServeFile(w, r, p) +} + +// allowCORS allows Cross Origin Resoruce Sharing from any origin. +// Don't do this without consideration in production systems. +func allowCORS(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if origin := r.Header.Get("Origin"); origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) + if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { + preflightHandler(w, r) + return + } + } + h.ServeHTTP(w, r) + }) +} + +func preflightHandler(w http.ResponseWriter, r *http.Request) { + headers := []string{"Content-Type", "Accept"} + w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) + methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"} + w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) + glog.Infof("preflight request for %s", r.URL.Path) + return +} + +// Run starts a HTTP server and blocks forever if successful. +func Run(address string, opts ...runtime.ServeMuxOption) error { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + mux := http.NewServeMux() + mux.HandleFunc("/swagger/", serveSwagger) + + gw, err := newGateway(ctx, opts...) if err != nil { return err } + mux.Handle("/", gw) - http.ListenAndServe(address, mux) + http.ListenAndServe(address, allowCORS(mux)) return nil }