Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Angular support #626

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions advanced.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,12 @@ services:
image: grpcweb/binary-client
ports:
- "8081:8081"
angular-client:
build:
context: ./
dockerfile: ./net/grpc/gateway/docker/angular_client/Dockerfile
depends_on:
- prereqs
image: grpcweb/angular-client
ports:
- "8081:8081"
185 changes: 185 additions & 0 deletions javascript/net/grpc/web/grpc_generator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@ void ReplaceCharacters(string *s, const char *remove, char replacewith) {
}
}

// Returns name in PascalCase to SCREAMING_SNAKE_CASE.
// e.g 'EchoServiceConfig' -> 'ECHO_SERVICE_CONFIG'
string PascalToConstCase(const string& s) {
if (s.empty()) {
return s;
}
// Ignore the first character. It's already uppercase and doesn't need an
// underscore.
string result;
result.push_back(s[0]);
for (int i = 1; i < s.size(); ++i) {
if (s[i] >= 'A' && s[i] <= 'Z') {
result.push_back('_');
result.push_back(s[i]);
} else {
result.push_back(s[i] - 'a' + 'A');
}
}
return result;
}

// The following function was copied from
// google/protobuf/src/google/protobuf/compiler/cpp/cpp_helpers.cc
Expand Down Expand Up @@ -661,6 +681,149 @@ void PrintES6Imports(Printer* printer, const FileDescriptor* file) {
printer->Print(vars, "} from './$base_name$_pb';\n\n");
}

void PrintAngularServiceImports(Printer* printer, const FileDescriptor* file,
const string base_file_name) {
printer->Print(
"import {Injectable, InjectionToken, Inject} from '@angular/core';\n");
printer->Print("import {Observable} from 'rxjs';\n\n");

if (!file->service_count()) {
return;
}

std::map<string, string> vars;
vars["base_file_name"] = base_file_name;

const ServiceDescriptor* service = file->service(0);
vars["service_name"] = service->name();

if (file->service_count() == 1) {
printer->Print(
vars, "import {$service_name$Client} from './$base_file_name$';\n\n");
return;
}

printer->Print("import {\n");
printer->Indent();
printer->Print(vars, "$service_name$");

for (int service_index = 1; service_index < file->service_count();
++service_index) {
const ServiceDescriptor* service = file->service(service_index);
vars["service_name"] = service->name();
printer->Print(vars, ",\n$service_name$Client");
}

printer->Outdent();
printer->Print(vars, "} from './$base_file_name$';\n\n");
}

void PrintAngularServiceMethod(Printer* printer, const MethodDescriptor* method,
std::map<string, string> vars) {
if (!method->client_streaming()) {
printer->Print(vars,
"$js_method_name$(\n"
" request: $input_type$,\n"
" metadata?: grpcWeb.Metadata\n"
"): Observable<$output_type$> {\n");
printer->Indent();
printer->Print("return new Observable(subscriber => {\n");
printer->Indent();

if (method->server_streaming()) {
vars["stream_object"] = "stream";
printer->Print(
vars,
"const $stream_object$ = this.client.$js_method_name$(request, "
"metadata);\n"
"$stream_object$.on('data', (response) => "
"subscriber.next(response));\n"
"$stream_object$.on('error', (err) => subscriber.error(err));\n"
"$stream_object$.on('status', (status) => {\n"
" if (status.code != grpcWeb.StatusCode.OK) {\n"
" subscriber.error({code: status.code, message: "
"status.details});\n"
" }\n"
"});\n"
"$stream_object$.on('end', () => subscriber.complete());\n");
} else {
vars["stream_object"] = "call";
printer->Print(
vars,
"const callback = (err, response) => {\n"
" if (err) {\n"
" subscriber.error(err);\n"
" } else {\n"
" subscriber.next(response);\n"
" subscriber.complete();\n"
" }\n"
"};\n"
"const $stream_object$ = this.client.$js_method_name$(request, "
"metadata, callback);\n");
}
printer->Print(vars,
"return () => { if ($stream_object$) { "
"$stream_object$.cancel(); } }\n");
printer->Outdent();
printer->Print("});\n");
printer->Outdent();
printer->Print("}\n\n");
}
}

void PrintAngularServiceClass(Printer* printer, const FileDescriptor* file) {
for (int service_index = 0; service_index < file->service_count();
++service_index) {
std::map<string, string> vars;
const ServiceDescriptor* service = file->service(service_index);
vars["service_name"] = service->name();
printer->Print(vars, "export interface $service_name$Config {\n");
printer->Print(
" hostname: string;\n"
" credentials: null | { [index: string]: string; };\n"
" options: null | { [index: string]: string; };\n");
printer->Print("};\n");
vars["config_name"] = PascalToConstCase(service->name()) + "_CONFIG";
printer->Print(
vars,
"export const $config_name$ = new "
"InjectionToken<$service_name$Config>('$config_name$');\n\n");

printer->Print(
"@Injectable({\n"
" providedIn: 'root',\n"
"})\n");
printer->Print(vars, "export class $service_name$ {\n");
printer->Indent();
printer->Print(vars,
"private client: $service_name$Client;\n\n"
"constructor(@Inject($config_name$) private config: "
"$service_name$Config) {\n"
" this.client = new $service_name$Client(\n"
" this.config.hostname,\n"
" this.config.credentials,\n"
" this.config.options);\n"
"}\n\n");
for (int method_index = 0; method_index < service->method_count();
++method_index) {
const MethodDescriptor* method = service->method(method_index);
vars["js_method_name"] = LowercaseFirstLetter(method->name());
vars["input_type"] = JSMessageType(method->input_type(), file);
vars["output_type"] = JSMessageType(method->output_type(), file);
PrintAngularServiceMethod(printer, method, vars);
}
printer->Outdent();
printer->Print("}\n");
}
}

void PrintAngularServiceFile(Printer* printer, const FileDescriptor* file,
const string base_file_name) {
PrintAngularServiceImports(printer, file, base_file_name);
PrintES6Imports(printer, file);
PrintAngularServiceClass(printer, file);
}

void PrintTypescriptFile(Printer* printer, const FileDescriptor* file,
std::map<string, string> vars) {
PrintES6Imports(printer, file);
Expand Down Expand Up @@ -1334,6 +1497,7 @@ class GrpcCodeGenerator : public CodeGenerator {
ImportStyle import_style;
bool generate_dts = false;
bool gen_multiple_files = false;
bool generate_angular = false;

for (size_t i = 0; i < options.size(); ++i) {
if (options[i].first == "out") {
Expand All @@ -1342,6 +1506,8 @@ class GrpcCodeGenerator : public CodeGenerator {
mode = options[i].second;
} else if (options[i].first == "import_style") {
import_style_str = options[i].second;
} else if (options[i].first == "framework") {
generate_angular = options[i].second == "angular";
} else if (options[i].first == "multiple_files") {
gen_multiple_files = options[i].second == "true";
} else {
Expand Down Expand Up @@ -1382,6 +1548,10 @@ class GrpcCodeGenerator : public CodeGenerator {
import_style = ImportStyle::CLOSURE;
} else if (import_style_str == "commonjs") {
import_style = ImportStyle::COMMONJS;
// Generate Typescript declarations for Angular.
if (generate_angular) {
generate_dts = true;
}
} else if (import_style_str == "commonjs+dts") {
import_style = ImportStyle::COMMONJS;
generate_dts = true;
Expand Down Expand Up @@ -1589,6 +1759,21 @@ class GrpcCodeGenerator : public CodeGenerator {
PrintGrpcWebDtsFile(&grpcweb_dts_printer, file);
}

if (generate_angular && import_style != ImportStyle::CLOSURE) {
string angular_service_file_name =
StripProto(file->name()) + ".service.pb.ts";

std::unique_ptr<ZeroCopyOutputStream> angular_service_output(
context->Open(angular_service_file_name));
Printer angular_service_printer(angular_service_output.get(), '$');

PrintFileHeader(&angular_service_printer, vars);

const string base_file_name = StripSuffixString(
file_name, import_style == TYPESCRIPT ? ".ts" : ".js");
PrintAngularServiceFile(&angular_service_printer, file, base_file_name);
}

return true;
}
};
Expand Down
36 changes: 36 additions & 0 deletions net/grpc/gateway/docker/angular_client/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

FROM grpcweb/prereqs

ARG EXAMPLE_DIR=/github/grpc-web/net/grpc/gateway/examples/echo
ARG ANGULAR_SERVICE_DIR=$EXAMPLE_DIR/angular-example/src/app/shared

RUN npm install -g @angular/cli

RUN curl -o chrome.deb -sSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN dpkg -i chrome.deb; apt-get -fy install

RUN protoc -I=$EXAMPLE_DIR echo.proto \
--js_out=import_style=commonjs:$ANGULAR_SERVICE_DIR \
--grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext,framework=angular:$ANGULAR_SERVICE_DIR

RUN cd $EXAMPLE_DIR/angular-example && \
npm install && \
ng build --prod && \
cp dist/angular-example/* /var/www/html

EXPOSE 8081
WORKDIR /var/www/html
CMD ["python", "-m", "SimpleHTTPServer", "8081"]
48 changes: 48 additions & 0 deletions net/grpc/gateway/examples/echo/angular-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out

# dependencies
/node_modules
package-lock.json
yarn.lock

# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings

# System Files
.DS_Store
Thumbs.db
27 changes: 27 additions & 0 deletions net/grpc/gateway/examples/echo/angular-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# AngularExample

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.0.

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.

## Code scaffolding

Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.

## Build

Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.

## Running unit tests

Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).

## Running end-to-end tests

Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).

## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
Loading