diff --git a/.circleci/config.yml b/.circleci/config.yml index d47c89625..80224a400 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,19 +8,23 @@ jobs: - run: name: Install protoc command: | - sudo apt-get update - sudo apt-get install protobuf-compiler + wget https://github.com/protocolbuffers/protobuf/releases/download/v3.10.0/protoc-3.10.0-linux-x86_64.zip + cd / && sudo unzip /tmp/protoc-3.10.0-linux-x86_64.zip + protoc --version + working_directory: /tmp - run: name: Run npm install command: | npm install - run: - name: Run baseline tests + name: Run generator unit tests command: | - sudo npm link + npm link npm test + environment: + NPM_CONFIG_PREFIX: /tmp/.npm-global - run: - name: Run showcase test for js users + name: Run showcase test for JavaScript users command: | cp -r typescript/test/protos ./.test-out-showcase cd .test-out-showcase @@ -32,7 +36,7 @@ jobs: npm install npm test - run: - name: Run showcase test for ts users + name: Run showcase test for TypeScript users command: | cd /home/circleci/project/.test-out-showcase cp -r ../typescript/test/test_application_ts ~/ @@ -40,10 +44,25 @@ jobs: cd ~/test_application_ts npm install npm test - - run: - name: Run unit tests + - run: + name: Run unit tests of the generated Showcase library + command: | + cd .test-out-showcase + npm install + npm test + - run: + name: Run unit tests of the generated KMS library command: | - cd .test-out-showcase/ + cp -r typescript/test/protos ./.test-out-keymanager + cd .test-out-keymanager + npm install + npm test + - run: + name: Run unit tests of the generated Text-to-Speech library + command: | + cp -r typescript/test/protos ./.test-out-texttospeech + cd .test-out-texttospeech + npm install npm test - run: name: Run linting diff --git a/.gitignore b/.gitignore index 7a841686a..7a512f9fa 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ pbjs-genfiles/ .test-out-keymanager .test-out-showcase .client_library +*test-out* diff --git a/package.json b/package.json index df1eed526..ab763f49d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "codecov": "c8 --reporter=lcov mocha build/test/unit && c8 report", "lint": "gts check", "clean": "gts clean", - "compile-protos": "pbjs -p node_modules/google-gax/protos -t static-module -o pbjs-genfiles/plugin.js google/protobuf/compiler/plugin.proto google/api/annotations.proto google/api/client.proto google/longrunning/operations.proto && pbts pbjs-genfiles/plugin.js -o pbjs-genfiles/plugin.d.ts", + "compile-protos": "pbjs -p protos -p node_modules/google-gax/protos -t static-module -o pbjs-genfiles/plugin.js google/protobuf/compiler/plugin.proto google/api/annotations.proto google/api/client.proto google/longrunning/operations.proto service_config.proto && pbts pbjs-genfiles/plugin.js -o pbjs-genfiles/plugin.d.ts", "compile": "tsc -p . && cp -r typescript/test/protos build/test/", "fix": "gts fix", "prepare": "npm run compile-protos && npm run compile", @@ -35,18 +35,18 @@ }, "homepage": "https://github.com/googleapis/gapic-generator-typescript#readme", "devDependencies": { - "@types/command-line-args": "^5.0.0", "@types/fs-extra": "^8.0.1", "@types/get-stdin": "^5.0.1", - "@types/mocha": "^5.2.7", + "@types/mocha": "^5.2.5", "@types/node": "^11.13.22", "@types/nunjucks": "^3.1.0", + "@types/object-hash": "^1.3.0", "@types/rimraf": "^2.0.2", + "@types/yargs": "^13.0.3", "assert-rejects": "^1.0.0", "c8": "^5.0.4", "codecov": "^3.6.1", "espower-typescript": "^9.0.0", - "fs-extra": "^8.1.0", "google-gax": "^1.7.5", "gts": "^1.0.0", "intelli-espower-loader": "^1.0.1", @@ -56,13 +56,11 @@ "typescript": "~3.6.0" }, "dependencies": { - "@types/fs-extra": "^8.0.1", - "@types/yargs": "^13.0.3", - "command-line-args": "^5.0.2", "file-system": "^2.2.2", "fs-extra": "^8.1.0", "get-stdin": "^7.0.0", "nunjucks": "^3.1.3", + "object-hash": "^2.0.0", "protobufjs": "^6.8.8", "yargs": "^14.2.0" } diff --git a/protos/service_config.proto b/protos/service_config.proto new file mode 100644 index 000000000..a78e0cd91 --- /dev/null +++ b/protos/service_config.proto @@ -0,0 +1,324 @@ +// Copyright 2016 The gRPC Authors +// +// 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 +// +// http://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. + +// A ServiceConfig is supplied when a service is deployed. It mostly contains +// parameters for how clients that connect to the service should behave (for +// example, the load balancing policy to use to pick between service replicas). +// +// The configuration options provided here act as overrides to automatically +// chosen option values. Service owners should be conservative in specifying +// options as the system is likely to choose better values for these options in +// the vast majority of cases. In other words, please specify a configuration +// option only if you really have to, and avoid copy-paste inclusion of configs. + +syntax = "proto3"; + +package grpc.service_config; + +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; +import "google/rpc/code.proto"; + +option java_package = "io.grpc.serviceconfig"; +option java_multiple_files = true; +option java_outer_classname = "ServiceConfigProto"; + +// Configuration for a method. +message MethodConfig { + // The names of the methods to which this configuration applies. There must + // be at least one name. Each name entry must be unique across the entire + // ClientConfig. If the 'method' field is empty, then this MethodConfig + // specifies the defaults for all methods for the specified service. + // + // For example, let's say that the service config contains the following + // MethodConfig entries: + // + // method_config { name { service: "MyService" } ... } + // method_config { name { service: "MyService" method: "Foo" } ... } + // + // For a request for MyService/Foo, we will use the second entry, because it + // exactly matches the service and method name. + // For a request for MyService/Bar, we will use the first entry, because it + // provides the default for all methods of MyService. + message Name { + string service = 1; // Required. Includes proto package name. + string method = 2; + } + repeated Name name = 1; + + // Whether RPCs sent to this method should wait until the connection is + // ready by default. If false, the RPC will abort immediately if there is + // a transient failure connecting to the server. Otherwise, gRPC will + // attempt to connect until the deadline is exceeded. + // + // The value specified via the gRPC client API will override the value + // set here. However, note that setting the value in the client API will + // also affect transient errors encountered during name resolution, which + // cannot be caught by the value here, since the service config is + // obtained by the gRPC client via name resolution. + google.protobuf.BoolValue wait_for_ready = 2; + + // The default timeout in seconds for RPCs sent to this method. This can be + // overridden in code. If no reply is received in the specified amount of + // time, the request is aborted and a DEADLINE_EXCEEDED error status + // is returned to the caller. + // + // The actual deadline used will be the minimum of the value specified here + // and the value set by the application via the gRPC client API. If either + // one is not set, then the other will be used. If neither is set, then the + // request has no deadline. + google.protobuf.Duration timeout = 3; + + // The maximum allowed payload size for an individual request or object in a + // stream (client->server) in bytes. The size which is measured is the + // serialized payload after per-message compression (but before stream + // compression) in bytes. This applies both to streaming and non-streaming + // requests. + // + // The actual value used is the minumum of the value specified here and the + // value set by the application via the gRPC client API. If either one is + // not set, then the other will be used. If neither is set, then the + // built-in default is used. + // + // If a client attempts to send an object larger than this value, it will not + // be sent and the client will see a ClientError. + // Note that 0 is a valid value, meaning that the request message + // must be empty. + google.protobuf.UInt32Value max_request_message_bytes = 4; + + // The maximum allowed payload size for an individual response or object in a + // stream (server->client) in bytes. The size which is measured is the + // serialized payload after per-message compression (but before stream + // compression) in bytes. This applies both to streaming and non-streaming + // requests. + // + // The actual value used is the minumum of the value specified here and the + // value set by the application via the gRPC client API. If either one is + // not set, then the other will be used. If neither is set, then the + // built-in default is used. + // + // If a server attempts to send an object larger than this value, it will not + // be sent, and a ServerError will be sent to the client instead. + // Note that 0 is a valid value, meaning that the response message + // must be empty. + google.protobuf.UInt32Value max_response_message_bytes = 5; + + // The retry policy for outgoing RPCs. + message RetryPolicy { + // The maximum number of RPC attempts, including the original attempt. + // + // This field is required and must be greater than 1. + // Any value greater than 5 will be treated as if it were 5. + uint32 max_attempts = 1; + + // Exponential backoff parameters. The initial retry attempt will occur at + // random(0, initial_backoff). In general, the nth attempt will occur at + // random(0, + // min(initial_backoff*backoff_multiplier**(n-1), max_backoff)). + // Required. Must be greater than zero. + google.protobuf.Duration initial_backoff = 2; + // Required. Must be greater than zero. + google.protobuf.Duration max_backoff = 3; + float backoff_multiplier = 4; // Required. Must be greater than zero. + + // The set of status codes which may be retried. + // + // This field is required and must be non-empty. + repeated google.rpc.Code retryable_status_codes = 5; + } + + // The hedging policy for outgoing RPCs. Hedged RPCs may execute more than + // once on the server, so only idempotent methods should specify a hedging + // policy. + message HedgingPolicy { + // The hedging policy will send up to max_requests RPCs. + // This number represents the total number of all attempts, including + // the original attempt. + // + // This field is required and must be greater than 1. + // Any value greater than 5 will be treated as if it were 5. + uint32 max_attempts = 1; + + // The first RPC will be sent immediately, but the max_requests-1 subsequent + // hedged RPCs will be sent at intervals of every hedging_delay. Set this + // to 0 to immediately send all max_requests RPCs. + google.protobuf.Duration hedging_delay = 2; + + // The set of status codes which indicate other hedged RPCs may still + // succeed. If a non-fatal status code is returned by the server, hedged + // RPCs will continue. Otherwise, outstanding requests will be canceled and + // the error returned to the client application layer. + // + // This field is optional. + repeated google.rpc.Code non_fatal_status_codes = 3; + } + + // Only one of retry_policy or hedging_policy may be set. If neither is set, + // RPCs will not be retried or hedged. + oneof retry_or_hedging_policy { + RetryPolicy retry_policy = 6; + HedgingPolicy hedging_policy = 7; + } +} + +// Configuration for pick_first LB policy. +message PickFirstConfig {} + +// Configuration for round_robin LB policy. +message RoundRobinConfig {} + +// Configuration for grpclb LB policy. +message GrpcLbConfig { + // Optional. What LB policy to use for routing between the backend + // addresses. If unset, defaults to round_robin. + // Currently, the only supported values are round_robin and pick_first. + // Note that this will be used both in balancer mode and in fallback mode. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig child_policy = 1; +} + +// Configuration for xds LB policy. +message XdsConfig { + // Required. Name of balancer to connect to. + string balancer_name = 1; + // Optional. What LB policy to use for intra-locality routing. + // If unset, will use whatever algorithm is specified by the balancer. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig child_policy = 2; + // Optional. What LB policy to use in fallback mode. If not + // specified, defaults to round_robin. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig fallback_policy = 3; +} + +// Selects LB policy and provides corresponding configuration. +// +// In general, all instances of this field should be repeated. +// Clients will iterate through the list in order and stop at the first +// policy that they support. This allows the service config to specify +// custom policies that may not be known to all clients. +message LoadBalancingConfig { + // Exactly one LB policy may be configured. + oneof policy { + // For each new LB policy supported by gRPC, a new field must be added + // here. The field's name must be the LB policy name and its type is a + // message that provides whatever configuration parameters are needed + // by the LB policy. The configuration message will be passed to the + // LB policy when it is instantiated on the client. + // + // If the LB policy does not require any configuration parameters, the + // message for that LB policy may be empty. + // + // Note that if an LB policy contains another nested LB policy + // (e.g., a gslb policy picks the cluster and then delegates to + // a round_robin policy to pick the backend within that cluster), its + // configuration message may include a nested instance of the + // LoadBalancingConfig message to configure the nested LB policy. + + PickFirstConfig pick_first = 4 [json_name = "pick_first"]; + + RoundRobinConfig round_robin = 1 [json_name = "round_robin"]; + + // gRPC lookaside load balancing. + // This will eventually be deprecated by the new xDS-based local + // balancing policy. + GrpcLbConfig grpclb = 3; + + // EXPERIMENTAL -- DO NOT USE + // xDS-based load balancing. + // The policy is known as xds_experimental while it is under development. + // It will be renamed to xds once it is ready for public use. + XdsConfig xds = 2; + // TODO(rekarthik): Deprecate this field after the xds policy + // is ready for public use. + XdsConfig xds_experimental = 5 [json_name = "xds_experimental"]; + + // Next available ID: 6 + } +} + +// A ServiceConfig represents information about a service but is not specific to +// any name resolver. +message ServiceConfig { + // Load balancing policy. + // + // Note that load_balancing_policy is deprecated in favor of + // load_balancing_config; the former will be used only if the latter + // is unset. + // + // If no LB policy is configured here, then the default is pick_first. + // If the policy name is set via the client API, that value overrides + // the value specified here. + // + // If the deprecated load_balancing_policy field is used, note that if the + // resolver returns at least one balancer address (as opposed to backend + // addresses), gRPC will use grpclb (see + // https://github.com/grpc/grpc/blob/master/doc/load-balancing.md), + // regardless of what policy is configured here. However, if the resolver + // returns at least one backend address in addition to the balancer + // address(es), the client may fall back to the requested policy if it + // is unable to reach any of the grpclb load balancers. + enum LoadBalancingPolicy { + UNSPECIFIED = 0; + ROUND_ROBIN = 1; + } + LoadBalancingPolicy load_balancing_policy = 1 [deprecated = true]; + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. If none + // are supported, the service config is considered invalid. + repeated LoadBalancingConfig load_balancing_config = 4; + + // Per-method configuration. + repeated MethodConfig method_config = 2; + + // If a RetryThrottlingPolicy is provided, gRPC will automatically throttle + // retry attempts and hedged RPCs when the client's ratio of failures to + // successes exceeds a threshold. + // + // For each server name, the gRPC client will maintain a token_count which is + // initially set to max_tokens. Every outgoing RPC (regardless of service or + // method invoked) will change token_count as follows: + // + // - Every failed RPC will decrement the token_count by 1. + // - Every successful RPC will increment the token_count by token_ratio. + // + // If token_count is less than or equal to max_tokens / 2, then RPCs will not + // be retried and hedged RPCs will not be sent. + message RetryThrottlingPolicy { + // The number of tokens starts at max_tokens. The token_count will always be + // between 0 and max_tokens. + // + // This field is required and must be greater than zero. + uint32 max_tokens = 1; + + // The amount of tokens to add on each successful RPC. Typically this will + // be some number between 0 and 1, e.g., 0.1. + // + // This field is required and must be greater than zero. Up to 3 decimal + // places are supported. + float token_ratio = 2; + } + RetryThrottlingPolicy retry_throttling = 3; + + message HealthCheckConfig { + // Service name to use in the health-checking request. + google.protobuf.StringValue service_name = 1; + } + HealthCheckConfig health_check_config = 5; + + // next available tag: 6 +} diff --git a/templates/typescript_gapic/src/$version/$service_client_config.json.njk b/templates/typescript_gapic/src/$version/$service_client_config.json.njk index 354f78474..411bdb1fc 100644 --- a/templates/typescript_gapic/src/$version/$service_client_config.json.njk +++ b/templates/typescript_gapic/src/$version/$service_client_config.json.njk @@ -2,33 +2,33 @@ "interfaces": { "{{ api.naming.protoPackage }}.{{ service.name }}": { "retry_codes": { - "idempotent": [ - "UNKNOWN", - "ABORTED" - ], - "non_idempotent": [ - "UNAVAILABLE" - ] +{%- set retryCodesComma = joiner() %} +{%- for prettyName in service.retryableCodeMap.getPrettyCodesNames() %} + {{- retryCodesComma() }} + "{{ prettyName }}": {{ service.retryableCodeMap.getCodesJSON(prettyName) | safe }} +{%- endfor %} }, "retry_params": { - "default": { - "initial_retry_delay_millis": 100, - "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, - "initial_rpc_timeout_millis": 20000, - "rpc_timeout_multiplier": 1.0, - "max_rpc_timeout_millis": 20000, - "total_timeout_millis": 600000 - } +{%- set retryParamsComma = joiner() %} +{%- for prettyName in service.retryableCodeMap.getPrettyParamsNames() %} + {{- retryParamsComma() }} + "{{ prettyName }}": {{ service.retryableCodeMap.getParamsJSON(prettyName) | safe }} +{%- endfor %} }, "methods": { {%- set comma = joiner() %} {%- for method in service.method %} {{- comma() }} "{{ method.name }}": { - "timeout_millis": 60000, - "retry_codes_name": "{{ method.idempotence }}", - "retry_params_name": "default" +{%- set optionComma = joiner() %} +{%- if method.timeoutMillis %} + {{- optionComma() }} + "timeout_millis": {{ method.timeoutMillis }} +{%- endif %} + {{- optionComma() }} + "retry_codes_name": "{{ method.retryableCodesName }}" + {{- optionComma() }} + "retry_params_name": "{{ method.retryParamsName }}" } {%- endfor %} } diff --git a/typescript/src/cli.ts b/typescript/src/cli.ts index 4225cbbdb..d1805c478 100644 --- a/typescript/src/cli.ts +++ b/typescript/src/cli.ts @@ -14,16 +14,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as commandLineArgs from 'command-line-args'; +import * as yargs from 'yargs'; import { Generator } from './generator'; async function main() { - const optionDefinitions: commandLineArgs.OptionDefinition[] = [ - { name: 'descriptor', type: String }, - ]; - const options = commandLineArgs(optionDefinitions); + const argv = yargs.argv; - if (options.descriptor) { + if (argv.descriptor) { console.error('Descriptor option is not yet supported.'); process.exit(1); } diff --git a/typescript/src/generator.ts b/typescript/src/generator.ts index 75ae961d6..34038df62 100644 --- a/typescript/src/generator.ts +++ b/typescript/src/generator.ts @@ -14,12 +14,16 @@ import * as getStdin from 'get-stdin'; import * as path from 'path'; +import * as fs from 'fs'; +import * as util from 'util'; import * as plugin from '../../pbjs-genfiles/plugin'; import { API } from './schema/api'; import { processTemplates } from './templater'; -import { commonPrefix } from './util'; +import { commonPrefix, duration } from './util'; + +const readFile = util.promisify(fs.readFile); const templateDirectory = path.join( __dirname, @@ -34,10 +38,49 @@ const templateDirectory = path.join( export class Generator { request: plugin.google.protobuf.compiler.CodeGeneratorRequest; response: plugin.google.protobuf.compiler.CodeGeneratorResponse; + grpcServiceConfig: plugin.grpc.service_config.ServiceConfig; constructor() { this.request = plugin.google.protobuf.compiler.CodeGeneratorRequest.create(); this.response = plugin.google.protobuf.compiler.CodeGeneratorResponse.create(); + this.grpcServiceConfig = plugin.grpc.service_config.ServiceConfig.create(); + } + + // Fixes gRPC service config to replace string google.protobuf.Duration + // to a proper Duration message, since protobufjs does not support + // string Durations such as "30s". + private static updateDuration(obj: { [key: string]: {} }) { + const fieldNames = [ + 'timeout', + 'initialBackoff', + 'maxBackoff', + 'hedgingDelay', + ]; + for (const key of Object.keys(obj)) { + if (fieldNames.includes(key) && typeof obj[key] === 'string') { + obj[key] = duration((obj[key] as unknown) as string); + } else if (typeof obj[key] === 'object') { + this.updateDuration(obj[key]); + } + } + } + + private async readGrpcServiceConfig(parameter: string) { + const match = parameter.match(/^["']?grpc-service-config=([^"]+)["']?$/); + if (!match) { + throw new Error(`Parameter ${parameter} was not recognized.`); + } + const filename = match[1]; + if (!fs.existsSync(filename)) { + throw new Error(`File ${filename} cannot be opened.`); + } + + const content = await readFile(filename); + const json = JSON.parse(content.toString()); + Generator.updateDuration(json); + this.grpcServiceConfig = plugin.grpc.service_config.ServiceConfig.fromObject( + json + ); } async initializeFromStdin() { @@ -45,9 +88,12 @@ export class Generator { this.request = plugin.google.protobuf.compiler.CodeGeneratorRequest.decode( inputBuffer ); + if (this.request.parameter) { + await this.readGrpcServiceConfig(this.request.parameter); + } } - addProtosToResponse() { + private addProtosToResponse() { const protoFilenames: string[] = []; for (const proto of this.request.protoFile) { if (proto.name) { @@ -60,7 +106,7 @@ export class Generator { this.response.file.push(protoList); } - buildAPIObject(): API { + private buildAPIObject(): API { const protoFilesToGenerate = this.request.protoFile.filter( pf => pf.name && this.request.fileToGenerate.includes(pf.name) ); @@ -71,7 +117,11 @@ export class Generator { if (packageName === '') { throw new Error('Cannot get package name to generate.'); } - const api = new API(this.request.protoFile, packageName); + const api = new API( + this.request.protoFile, + packageName, + this.grpcServiceConfig + ); return api; } @@ -88,7 +138,6 @@ export class Generator { this.addProtosToResponse(); const api = this.buildAPIObject(); await this.processTemplates(api); - // TODO: error handling const outputBuffer = plugin.google.protobuf.compiler.CodeGeneratorResponse.encode( this.response diff --git a/typescript/src/schema/api.ts b/typescript/src/schema/api.ts index 574c88b6f..ebab868e2 100644 --- a/typescript/src/schema/api.ts +++ b/typescript/src/schema/api.ts @@ -18,7 +18,8 @@ export class API { constructor( fileDescriptors: plugin.google.protobuf.IFileDescriptorProto[], - packageName: string + packageName: string, + grpcServiceConfig: plugin.grpc.service_config.ServiceConfig ) { this.naming = new Naming( fileDescriptors.filter( @@ -29,7 +30,7 @@ export class API { .filter(fd => fd.name) .reduce( (map, fd) => { - map[fd.name!] = new Proto(fd, packageName); + map[fd.name!] = new Proto(fd, packageName, grpcServiceConfig); return map; }, {} as ProtosMap diff --git a/typescript/src/schema/proto.ts b/typescript/src/schema/proto.ts index aeb78b81d..46d234647 100644 --- a/typescript/src/schema/proto.ts +++ b/typescript/src/schema/proto.ts @@ -1,9 +1,30 @@ import * as plugin from '../../../pbjs-genfiles/plugin'; import { CommentsMap } from './comments'; +import * as objectHash from 'object-hash'; +import { milliseconds } from '../util'; + +const defaultNonIdempotentRetryCodesName = 'non_idempotent'; +const defaultNonIdempotentCodes: plugin.google.rpc.Code[] = []; +const defaultIdempotentRetryCodesName = 'idempotent'; +const defaultIdempotentCodes = [ + plugin.google.rpc.Code.DEADLINE_EXCEEDED, + plugin.google.rpc.Code.UNAVAILABLE, +]; +const defaultParametersName = 'default'; +const defaultParameters = { + initial_retry_delay_millis: 100, + retry_delay_multiplier: 1.3, + max_retry_delay_millis: 60000, + // note: the following four parameters are unused but currently required by google-gax. + // setting them to some big safe default values. + initial_rpc_timeout_millis: 20000, + rpc_timeout_multiplier: 1.0, + max_rpc_timeout_millis: 20000, + total_timeout_millis: 600000, +}; interface MethodDescriptorProto extends plugin.google.protobuf.IMethodDescriptorProto { - idempotence: 'idempotent' | 'non_idempotent'; longRunning?: plugin.google.longrunning.IOperationInfo; longRunningResponseType?: string; longRunningMetadataType?: string; @@ -17,10 +38,114 @@ interface MethodDescriptorProto inputInterface: string; outputInterface: string; comments: string; + methodConfig: plugin.grpc.service_config.MethodConfig; + retryableCodesName: string; + retryParamsName: string; + timeoutMillis?: number; +} + +export class RetryableCodeMap { + codeEnumMapping: { [index: string]: string }; + uniqueCodesNamesMap: { [uniqueName: string]: string }; + prettyCodesNamesMap: { [prettyName: string]: string[] }; + uniqueParamsNamesMap: { [uniqueName: string]: string }; + prettyParamNamesMap: { [prettyName: string]: {} }; + + constructor() { + this.uniqueCodesNamesMap = {}; + this.prettyCodesNamesMap = {}; + this.uniqueParamsNamesMap = {}; + this.prettyParamNamesMap = {}; + + // build reverse mapping for enum: 0 => OK, 1 => CANCELLED, etc. + this.codeEnumMapping = {}; + const allCodes = Object.keys(plugin.google.rpc.Code); + for (const code of allCodes) { + this.codeEnumMapping[ + ((plugin.google.rpc.Code as unknown) as { + [key: string]: plugin.google.rpc.Code; + })[code].toString() + ] = code; + } + + // generate some pre-defined code sets for compatibility with existing configs + this.getRetryableCodesName( + defaultNonIdempotentCodes, + defaultNonIdempotentRetryCodesName + ); + this.getRetryableCodesName( + defaultIdempotentCodes, + defaultIdempotentRetryCodesName + ); + this.getParamsName(defaultParameters, 'default'); + } + + private buildUniqueCodesName( + retryableStatusCodes: plugin.google.rpc.Code[] + ): string { + // generate an unique readable name for the given retryable set of codes + const sortedCodes = retryableStatusCodes.sort( + (a, b) => Number(a) - Number(b) + ); + const uniqueName = sortedCodes + .map(code => this.codeEnumMapping[code]) + .join('_') + .toSnakeCase(); + return uniqueName; + } + + private buildUniqueParamsName(params: {}): string { + // generate an unique not so readable name for the given set of parameters + return objectHash(params); + } + + getRetryableCodesName( + retryableStatusCodes: plugin.google.rpc.Code[], + suggestedName?: string + ): string { + const uniqueName = this.buildUniqueCodesName(retryableStatusCodes); + const prettyName = + this.uniqueCodesNamesMap[uniqueName] || suggestedName || uniqueName; + if (!this.uniqueCodesNamesMap[uniqueName]) { + this.uniqueCodesNamesMap[uniqueName] = prettyName; + this.prettyCodesNamesMap[prettyName] = retryableStatusCodes.map( + code => this.codeEnumMapping[code] + ); + } + return prettyName; + } + + getParamsName(params: {}, suggestedName?: string): string { + const uniqueName = this.buildUniqueParamsName(params); + const prettyName = + this.uniqueParamsNamesMap[uniqueName] || suggestedName || uniqueName; + if (!this.uniqueParamsNamesMap[uniqueName]) { + this.uniqueParamsNamesMap[uniqueName] = prettyName; + this.prettyParamNamesMap[prettyName] = params; + } + return prettyName; + } + + getPrettyCodesNames(): string[] { + return Object.keys(this.prettyCodesNamesMap); + } + + getCodesJSON(prettyName: string): string { + return JSON.stringify(this.prettyCodesNamesMap[prettyName]); + } + + getPrettyParamsNames(): string[] { + return Object.keys(this.prettyParamNamesMap); + } + + getParamsJSON(prettyName: string): string { + return JSON.stringify(this.prettyParamNamesMap[prettyName]); + } } interface ServiceDescriptorProto extends plugin.google.protobuf.IServiceDescriptorProto { + packageName: string; method: MethodDescriptorProto[]; simpleMethods: MethodDescriptorProto[]; longRunning: MethodDescriptorProto[]; @@ -33,6 +158,9 @@ interface ServiceDescriptorProto port: number; oauthScopes: string[]; comments: string; + commentsMap: CommentsMap; + retryableCodeMap: RetryableCodeMap; + grpcServiceConfig: plugin.grpc.service_config.ServiceConfig; } export interface ServicesMap { @@ -49,18 +177,6 @@ export interface EnumsMap { // flag, long running operation info, pagination, and streaming, to all the // methods of the given service, to use in templates. -function idempotence(method: MethodDescriptorProto) { - if ( - method.options && - method.options['.google.api.http'] && - (method.options['.google.api.http']['get'] || - method.options['.google.api.http']['put']) - ) { - return 'idempotent'; - } - return 'non_idempotent'; -} - function longrunning(method: MethodDescriptorProto) { if (method.options && method.options['.google.longrunning.operationInfo']) { return method.options['.google.longrunning.operationInfo']!; @@ -157,19 +273,43 @@ function toInterface(type: string) { // Convert long running type to the interface // eg: WaitResponse -> .google.showcase.v1beta1.IWaitResponse // eg: WaitMetadata -> .google.showcase.v1beta1.IWaitMetadata - function toLRInterface(type: string, inputType: string) { return inputType.replace(/\.([^.]+)$/, '.I' + type); } +function getMethodConfig( + grpcServiceConfig: plugin.grpc.service_config.ServiceConfig, + serviceName: string, + methodName: string +): plugin.grpc.service_config.MethodConfig { + let exactMatch: plugin.grpc.service_config.IMethodConfig | undefined; + let serviceMatch: plugin.grpc.service_config.IMethodConfig | undefined; + for (const config of grpcServiceConfig.methodConfig) { + if (!config.name) { + continue; + } + for (const name of config.name) { + if (name.service === serviceName && !name.method) { + serviceMatch = config; + } + if (name.service === serviceName && name.method === methodName) { + exactMatch = config; + } + } + } + const result = plugin.grpc.service_config.MethodConfig.fromObject( + exactMatch || serviceMatch || {} + ); + return result; +} + function augmentMethod( messages: MessagesMap, - method: MethodDescriptorProto, - commentsMap: CommentsMap + service: ServiceDescriptorProto, + method: MethodDescriptorProto ) { method = Object.assign( { - idempotence: idempotence(method), longRunning: longrunning(method), longRunningResponseType: longRunningResponseType(method), longRunningMetadataType: longRunningMetadataType(method), @@ -178,22 +318,77 @@ function augmentMethod( pagingResponseType: pagingResponseType(messages, method), inputInterface: toInterface(method.inputType!), outputInterface: toInterface(method.outputType!), - comments: commentsMap.getMethodComments(method.name!), + comments: service.commentsMap.getMethodComments(method.name!), + methodConfig: getMethodConfig( + service.grpcServiceConfig, + `${service.packageName}.${service.name!}`, + method.name! + ), + retryableCodesName: defaultNonIdempotentRetryCodesName, + retryParamsName: defaultParametersName, }, method ) as MethodDescriptorProto; + if ( + method.methodConfig.retryPolicy && + method.methodConfig.retryPolicy.retryableStatusCodes + ) { + method.retryableCodesName = service.retryableCodeMap.getRetryableCodesName( + method.methodConfig.retryPolicy.retryableStatusCodes + ); + } + if (method.methodConfig.retryPolicy) { + // converting retry parameters to the syntax google-gax supports + const retryParams: { [key: string]: number } = {}; + if (method.methodConfig.retryPolicy.initialBackoff) { + retryParams.initial_retry_delay_millis = milliseconds( + method.methodConfig.retryPolicy.initialBackoff + ); + } + if (method.methodConfig.retryPolicy.backoffMultiplier) { + retryParams.retry_delay_multiplier = + method.methodConfig.retryPolicy.backoffMultiplier; + } + if (method.methodConfig.retryPolicy.maxBackoff) { + retryParams.max_retry_delay_millis = milliseconds( + method.methodConfig.retryPolicy.maxBackoff + ); + } + // note: the following four parameters are unused but currently required by google-gax. + // setting them to some big safe default values. + retryParams.initial_rpc_timeout_millis = + defaultParameters.initial_rpc_timeout_millis; + retryParams.rpc_timeout_multiplier = + defaultParameters.rpc_timeout_multiplier; + retryParams.max_rpc_timeout_millis = + defaultParameters.max_rpc_timeout_millis; + retryParams.total_timeout_millis = defaultParameters.total_timeout_millis; + + method.retryParamsName = service.retryableCodeMap.getParamsName( + retryParams + ); + } + if (method.methodConfig.timeout) { + method.timeoutMillis = milliseconds(method.methodConfig.timeout); + } return method; } function augmentService( messages: MessagesMap, + packageName: string, service: plugin.google.protobuf.IServiceDescriptorProto, - commentsMap: CommentsMap + commentsMap: CommentsMap, + grpcServiceConfig: plugin.grpc.service_config.ServiceConfig ) { const augmentedService = service as ServiceDescriptorProto; + augmentedService.packageName = packageName; augmentedService.comments = commentsMap.getServiceComment(service.name!); + augmentedService.commentsMap = commentsMap; + augmentedService.retryableCodeMap = new RetryableCodeMap(); + augmentedService.grpcServiceConfig = grpcServiceConfig; augmentedService.method = augmentedService.method.map(method => - augmentMethod(messages, method, commentsMap) + augmentMethod(messages, augmentedService, method) ); augmentedService.simpleMethods = augmentedService.method.filter( method => @@ -250,12 +445,12 @@ export class Proto { messages: MessagesMap = {}; enums: EnumsMap = {}; fileToGenerate: boolean; - commentsMap: CommentsMap; // TODO: need to store metadata? address? constructor( fd: plugin.google.protobuf.IFileDescriptorProto, - packageName: string + packageName: string, + grpcServiceConfig: plugin.grpc.service_config.ServiceConfig ) { fd.enumType = fd.enumType || []; fd.messageType = fd.messageType || []; @@ -286,10 +481,18 @@ export class Proto { this.fileToGenerate = fd.package ? fd.package.startsWith(packageName) : false; - this.commentsMap = new CommentsMap(fd); + const commentsMap = new CommentsMap(fd); this.services = fd.service .filter(service => service.name) - .map(service => augmentService(this.messages, service, this.commentsMap)) + .map(service => + augmentService( + this.messages, + packageName, + service, + commentsMap, + grpcServiceConfig + ) + ) .reduce( (map, service) => { map[service.name!] = service; diff --git a/typescript/src/start_script.ts b/typescript/src/start_script.ts index bdf84237a..77deb5c58 100755 --- a/typescript/src/start_script.ts +++ b/typescript/src/start_script.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node + // Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +16,7 @@ import { execFileSync } from 'child_process'; import * as path from 'path'; -import { argv } from 'yargs'; +import * as yargs from 'yargs'; import * as fs from 'fs-extra'; const fileSystem = require('file-system'); @@ -28,24 +29,26 @@ const GOOGLE_GAX_PROTOS_DIR = path.join( 'protos' ); -// Add folder of plugin to PATH -process.env['PATH'] = __dirname + path.delimiter + process.env['PATH']; - -let outputDir = ''; -if (argv.output_dir) { - outputDir = argv.output_dir as string; -} else { - console.error('Output directory is required: --output_dir path'); - process.exit(1); -} +const argv = yargs + .array('I') + .nargs('I', 1) + .alias('proto_path', 'I') + .alias('proto-path', 'I') + .demandOption('output_dir') + .describe('I', 'Include directory to pass to protoc') + .alias('output-dir', 'output_dir') + .describe('output_dir', 'Path to a directory for the generated code') + .alias('grpc-service-config', 'grpc_service_config') + .describe('grpc-service-config', 'Path to gRPC service config JSON') + .usage(`Usage: $0 -I /path/to/googleapis \\ + --output_dir /path/to/output_directory \\ + google/example/api/v1/api.proto`).argv; +const outputDir = argv.outputDir as string; +const grpcServiceConfig = argv.grpcServiceConfig as string | undefined; const protoDirs: string[] = []; if (argv.I) { - if (Array.isArray(argv.I)) { - protoDirs.push(...argv.I); - } else { - protoDirs.push(argv.I as string); - } + protoDirs.push(...(argv.I as string[])); } const protoDirsArg = protoDirs.map(dir => `-I${dir}`); @@ -57,10 +60,17 @@ if (Array.isArray(argv._)) { } // run protoc command to generate client library +const cliPath = path.join(__dirname, 'cli.js'); const protocCommand = [ `-I${GOOGLE_GAX_PROTOS_DIR}`, + `--plugin=protoc-gen-typescript_gapic=${cliPath}`, `--typescript_gapic_out=${outputDir}`, ]; +if (grpcServiceConfig) { + protocCommand.push( + `--typescript_gapic_opt="grpc-service-config=${grpcServiceConfig}"` + ); +} protocCommand.push(...protoDirsArg); protocCommand.push(...protoFiles); try { diff --git a/typescript/src/templater.ts b/typescript/src/templater.ts index a6d56944e..635f65e1b 100644 --- a/typescript/src/templater.ts +++ b/typescript/src/templater.ts @@ -43,7 +43,19 @@ function renderFile( templateName: string, renderParameters: {} ) { - const processed = nunjucks.render(templateName, renderParameters); + let processed = nunjucks.render(templateName, renderParameters); + // Pretty-print generated JSON files + if (targetFilename.match(/\.json$/i)) { + try { + const json = JSON.parse(processed); + const pretty = JSON.stringify(json, null, ' ') + '\n'; + processed = pretty; + } catch (err) { + console.warn( + `The generated JSON file ${targetFilename} does not look like a valid JSON: ${err.toString()}` + ); + } + } const output = plugin.google.protobuf.compiler.CodeGeneratorResponse.File.create(); output.name = targetFilename; output.content = processed; diff --git a/typescript/src/util.ts b/typescript/src/util.ts index b0191c89d..b003c07f3 100644 --- a/typescript/src/util.ts +++ b/typescript/src/util.ts @@ -1,3 +1,5 @@ +import * as plugin from '../../pbjs-genfiles/plugin'; + export function commonPrefix(strings: string[]): string { if (strings.length === 0) { return ''; @@ -15,6 +17,46 @@ export function commonPrefix(strings: string[]): string { return result; } +// Convert a string Duration, e.g. "600s", to a proper protobuf type since +// protobufjs does not support it at this moment. +export function duration(text: string): plugin.google.protobuf.Duration { + const multipliers: { [suffix: string]: number } = { + s: 1, + m: 60, + h: 60 * 60, + d: 60 * 60 * 24, + }; + const match = text.match(/^([\d.]+)([smhd])$/); + if (!match) { + throw new Error(`Cannot parse "${text}" into google.protobuf.Duration.`); + } + const float = Number(match[1]); + const suffix = match[2]; + const multiplier = multipliers[suffix]; + const seconds = float * multiplier; + const floor = Math.floor(seconds); + const frac = seconds - floor; + const result = plugin.google.protobuf.Duration.fromObject({ + seconds: floor, + nanos: frac * 1e9, + }); + return result; +} + +// Convert a Duration to (possibly fractional) seconds. +export function seconds(duration: plugin.google.protobuf.IDuration): number { + return Number(duration.seconds || 0) + Number(duration.nanos || 0) * 1e-9; +} + +// Convert a Duration to (possibly fractional) milliseconds. +export function milliseconds( + duration: plugin.google.protobuf.IDuration +): number { + return ( + Number(duration.seconds || 0) * 1000 + Number(duration.nanos || 0) * 1e-6 + ); +} + String.prototype.capitalize = function(this: string): string { if (this.length === 0) { return this; diff --git a/typescript/test/protos/google/cloud/texttospeech/v1/cloud_tts.proto b/typescript/test/protos/google/cloud/texttospeech/v1/cloud_tts.proto new file mode 100644 index 000000000..6263da4ab --- /dev/null +++ b/typescript/test/protos/google/cloud/texttospeech/v1/cloud_tts.proto @@ -0,0 +1,253 @@ +// Copyright 2019 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 +// +// http://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. +// + +syntax = "proto3"; + +package google.cloud.texttospeech.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; + +option cc_enable_arenas = true; +option csharp_namespace = "Google.Cloud.TextToSpeech.V1"; +option go_package = "google.golang.org/genproto/googleapis/cloud/texttospeech/v1;texttospeech"; +option java_multiple_files = true; +option java_outer_classname = "TextToSpeechProto"; +option java_package = "com.google.cloud.texttospeech.v1"; +option php_namespace = "Google\\Cloud\\TextToSpeech\\V1"; + +// Service that implements Google Cloud Text-to-Speech API. +service TextToSpeech { + option (google.api.default_host) = "texttospeech.googleapis.com"; + option (google.api.oauth_scopes) = "https://www.googleapis.com/auth/cloud-platform"; + + // Returns a list of Voice supported for synthesis. + rpc ListVoices(ListVoicesRequest) returns (ListVoicesResponse) { + option (google.api.http) = { + get: "/v1/voices" + }; + option (google.api.method_signature) = "language_code"; + } + + // Synthesizes speech synchronously: receive results after all text input + // has been processed. + rpc SynthesizeSpeech(SynthesizeSpeechRequest) returns (SynthesizeSpeechResponse) { + option (google.api.http) = { + post: "/v1/text:synthesize" + body: "*" + }; + option (google.api.method_signature) = "input,voice,audio_config"; + } +} + +// The top-level message sent by the client for the `ListVoices` method. +message ListVoicesRequest { + // Optional. Recommended. + // [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tag. If + // specified, the ListVoices call will only return voices that can be used to + // synthesize this language_code. E.g. when specifying "en-NZ", you will get + // supported "en-*" voices; when specifying "no", you will get supported + // "no-*" (Norwegian) and "nb-*" (Norwegian Bokmal) voices; specifying "zh" + // will also get supported "cmn-*" voices; specifying "zh-hk" will also get + // supported "yue-*" voices. + string language_code = 1 [(google.api.field_behavior) = OPTIONAL]; +} + +// Gender of the voice as described in +// [SSML voice element](https://www.w3.org/TR/speech-synthesis11/#edef_voice). +enum SsmlVoiceGender { + // An unspecified gender. + // In VoiceSelectionParams, this means that the client doesn't care which + // gender the selected voice will have. In the Voice field of + // ListVoicesResponse, this may mean that the voice doesn't fit any of the + // other categories in this enum, or that the gender of the voice isn't known. + SSML_VOICE_GENDER_UNSPECIFIED = 0; + + // A male voice. + MALE = 1; + + // A female voice. + FEMALE = 2; + + // A gender-neutral voice. + NEUTRAL = 3; +} + +// Configuration to set up audio encoder. The encoding determines the output +// audio format that we'd like. +enum AudioEncoding { + // Not specified. Will return result [google.rpc.Code.INVALID_ARGUMENT][]. + AUDIO_ENCODING_UNSPECIFIED = 0; + + // Uncompressed 16-bit signed little-endian samples (Linear PCM). + // Audio content returned as LINEAR16 also contains a WAV header. + LINEAR16 = 1; + + // MP3 audio at 32kbps. + MP3 = 2; + + // Opus encoded audio wrapped in an ogg container. The result will be a + // file which can be played natively on Android, and in browsers (at least + // Chrome and Firefox). The quality of the encoding is considerably higher + // than MP3 while using approximately the same bitrate. + OGG_OPUS = 3; +} + +// The message returned to the client by the `ListVoices` method. +message ListVoicesResponse { + // The list of voices. + repeated Voice voices = 1; +} + +// Description of a voice supported by the TTS service. +message Voice { + // The languages that this voice supports, expressed as + // [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tags (e.g. + // "en-US", "es-419", "cmn-tw"). + repeated string language_codes = 1; + + // The name of this voice. Each distinct voice has a unique name. + string name = 2; + + // The gender of this voice. + SsmlVoiceGender ssml_gender = 3; + + // The natural sample rate (in hertz) for this voice. + int32 natural_sample_rate_hertz = 4; +} + +// The top-level message sent by the client for the `SynthesizeSpeech` method. +message SynthesizeSpeechRequest { + // Required. The Synthesizer requires either plain text or SSML as input. + SynthesisInput input = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The desired voice of the synthesized audio. + VoiceSelectionParams voice = 2 [(google.api.field_behavior) = REQUIRED]; + + // Required. The configuration of the synthesized audio. + AudioConfig audio_config = 3 [(google.api.field_behavior) = REQUIRED]; +} + +// Contains text input to be synthesized. Either `text` or `ssml` must be +// supplied. Supplying both or neither returns +// [google.rpc.Code.INVALID_ARGUMENT][]. The input size is limited to 5000 +// characters. +message SynthesisInput { + // The input source, which is either plain text or SSML. + oneof input_source { + // The raw text to be synthesized. + string text = 1; + + // The SSML document to be synthesized. The SSML document must be valid + // and well-formed. Otherwise the RPC will fail and return + // [google.rpc.Code.INVALID_ARGUMENT][]. For more information, see + // [SSML](/speech/text-to-speech/docs/ssml). + string ssml = 2; + } +} + +// Description of which voice to use for a synthesis request. +message VoiceSelectionParams { + // Required. The language (and potentially also the region) of the voice expressed as a + // [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tag, e.g. + // "en-US". This should not include a script tag (e.g. use + // "cmn-cn" rather than "cmn-Hant-cn"), because the script will be inferred + // from the input provided in the SynthesisInput. The TTS service + // will use this parameter to help choose an appropriate voice. Note that + // the TTS service may choose a voice with a slightly different language code + // than the one selected; it may substitute a different region + // (e.g. using en-US rather than en-CA if there isn't a Canadian voice + // available), or even a different language, e.g. using "nb" (Norwegian + // Bokmal) instead of "no" (Norwegian)". + string language_code = 1 [(google.api.field_behavior) = REQUIRED]; + + // The name of the voice. If not set, the service will choose a + // voice based on the other parameters such as language_code and gender. + string name = 2; + + // The preferred gender of the voice. If not set, the service will + // choose a voice based on the other parameters such as language_code and + // name. Note that this is only a preference, not requirement; if a + // voice of the appropriate gender is not available, the synthesizer should + // substitute a voice with a different gender rather than failing the request. + SsmlVoiceGender ssml_gender = 3; +} + +// Description of audio data to be synthesized. +message AudioConfig { + // Required. The format of the audio byte stream. + AudioEncoding audio_encoding = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Input only. Speaking rate/speed, in the range [0.25, 4.0]. 1.0 is + // the normal native speed supported by the specific voice. 2.0 is twice as + // fast, and 0.5 is half as fast. If unset(0.0), defaults to the native 1.0 + // speed. Any other values < 0.25 or > 4.0 will return an error. + double speaking_rate = 2 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; + + // Optional. Input only. Speaking pitch, in the range [-20.0, 20.0]. 20 means + // increase 20 semitones from the original pitch. -20 means decrease 20 + // semitones from the original pitch. + double pitch = 3 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; + + // Optional. Input only. Volume gain (in dB) of the normal native volume + // supported by the specific voice, in the range [-96.0, 16.0]. If unset, or + // set to a value of 0.0 (dB), will play at normal native signal amplitude. A + // value of -6.0 (dB) will play at approximately half the amplitude of the + // normal native signal amplitude. A value of +6.0 (dB) will play at + // approximately twice the amplitude of the normal native signal amplitude. + // Strongly recommend not to exceed +10 (dB) as there's usually no effective + // increase in loudness for any value greater than that. + double volume_gain_db = 4 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; + + // Optional. The synthesis sample rate (in hertz) for this audio. When this is + // specified in SynthesizeSpeechRequest, if this is different from the voice's + // natural sample rate, then the synthesizer will honor this request by + // converting to the desired sample rate (which might result in worse audio + // quality), unless the specified sample rate is not supported for the + // encoding chosen, in which case it will fail the request and return + // [google.rpc.Code.INVALID_ARGUMENT][]. + int32 sample_rate_hertz = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Input only. An identifier which selects 'audio effects' profiles + // that are applied on (post synthesized) text to speech. Effects are applied + // on top of each other in the order they are given. See + // [audio + // profiles](https://cloud.google.com/text-to-speech/docs/audio-profiles) for + // current supported profile ids. + repeated string effects_profile_id = 6 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; +} + +// The message returned to the client by the `SynthesizeSpeech` method. +message SynthesizeSpeechResponse { + // The audio data bytes encoded as specified in the request, including the + // header for encodings that are wrapped in containers (e.g. MP3, OGG_OPUS). + // For LINEAR16 audio, we include the WAV header. Note: as + // with all bytes fields, protobuffers use a pure binary representation, + // whereas JSON representations use base64. + bytes audio_content = 1; +} diff --git a/typescript/test/protos/google/cloud/texttospeech/v1/texttospeech_grpc_service_config.json b/typescript/test/protos/google/cloud/texttospeech/v1/texttospeech_grpc_service_config.json new file mode 100755 index 000000000..aedaab2d1 --- /dev/null +++ b/typescript/test/protos/google/cloud/texttospeech/v1/texttospeech_grpc_service_config.json @@ -0,0 +1,26 @@ +{ + "methodConfig": [ + { + "name": [ + { + "service": "google.cloud.texttospeech.v1.TextToSpeech", + "method": "ListVoices" + }, + { + "service": "google.cloud.texttospeech.v1.TextToSpeech", + "method": "SynthesizeSpeech" + } + ], + "timeout": "600s", + "retryPolicy": { + "initialBackoff": "0.100s", + "maxBackoff": "60s", + "backoffMultiplier": 1.3, + "retryableStatusCodes": [ + "UNAVAILABLE", + "DEADLINE_EXCEEDED" + ] + } + } + ] +} diff --git a/typescript/test/testdata/keymanager/src/v1/key_management_service_client_config.json.baseline b/typescript/test/testdata/keymanager/src/v1/key_management_service_client_config.json.baseline index f0d2cc33a..4ff506aef 100644 --- a/typescript/test/testdata/keymanager/src/v1/key_management_service_client_config.json.baseline +++ b/typescript/test/testdata/keymanager/src/v1/key_management_service_client_config.json.baseline @@ -2,11 +2,9 @@ "interfaces": { "google.cloud.kms.v1.KeyManagementService": { "retry_codes": { + "non_idempotent": [], "idempotent": [ - "UNKNOWN", - "ABORTED" - ], - "non_idempotent": [ + "DEADLINE_EXCEEDED", "UNAVAILABLE" ] }, @@ -16,124 +14,101 @@ "retry_delay_multiplier": 1.3, "max_retry_delay_millis": 60000, "initial_rpc_timeout_millis": 20000, - "rpc_timeout_multiplier": 1.0, + "rpc_timeout_multiplier": 1, "max_rpc_timeout_millis": 20000, "total_timeout_millis": 600000 } }, "methods": { "ListKeyRings": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "ListCryptoKeys": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "ListCryptoKeyVersions": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "ListImportJobs": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "GetKeyRing": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "GetCryptoKey": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "GetCryptoKeyVersion": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "GetPublicKey": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "GetImportJob": { - "timeout_millis": 60000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "CreateKeyRing": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "CreateCryptoKey": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "CreateCryptoKeyVersion": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "ImportCryptoKeyVersion": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "CreateImportJob": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "UpdateCryptoKey": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "UpdateCryptoKeyVersion": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "Encrypt": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "Decrypt": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "AsymmetricSign": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "AsymmetricDecrypt": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "UpdateCryptoKeyPrimaryVersion": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "DestroyCryptoKeyVersion": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "RestoreCryptoKeyVersion": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" } diff --git a/typescript/test/testdata/keymanager/tsconfig.json.baseline b/typescript/test/testdata/keymanager/tsconfig.json.baseline index 2d9b83e3f..653c21079 100644 --- a/typescript/test/testdata/keymanager/tsconfig.json.baseline +++ b/typescript/test/testdata/keymanager/tsconfig.json.baseline @@ -4,7 +4,10 @@ "rootDir": ".", "outDir": "build", "resolveJsonModule": true, - "lib": ["es2016", "dom"] + "lib": [ + "es2016", + "dom" + ] }, "include": [ "src/*.ts", diff --git a/typescript/test/testdata/showcase/src/v1beta1/echo_client_config.json.baseline b/typescript/test/testdata/showcase/src/v1beta1/echo_client_config.json.baseline index 096dcd2ae..ec817cb21 100644 --- a/typescript/test/testdata/showcase/src/v1beta1/echo_client_config.json.baseline +++ b/typescript/test/testdata/showcase/src/v1beta1/echo_client_config.json.baseline @@ -2,11 +2,9 @@ "interfaces": { "google.showcase.v1beta1.Echo": { "retry_codes": { + "non_idempotent": [], "idempotent": [ - "UNKNOWN", - "ABORTED" - ], - "non_idempotent": [ + "DEADLINE_EXCEEDED", "UNAVAILABLE" ] }, @@ -16,39 +14,33 @@ "retry_delay_multiplier": 1.3, "max_retry_delay_millis": 60000, "initial_rpc_timeout_millis": 20000, - "rpc_timeout_multiplier": 1.0, + "rpc_timeout_multiplier": 1, "max_rpc_timeout_millis": 20000, "total_timeout_millis": 600000 } }, "methods": { "Echo": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "Expand": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "Collect": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "Chat": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "PagedExpand": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "Wait": { - "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" } diff --git a/typescript/test/testdata/showcase/tsconfig.json.baseline b/typescript/test/testdata/showcase/tsconfig.json.baseline index 2d9b83e3f..653c21079 100644 --- a/typescript/test/testdata/showcase/tsconfig.json.baseline +++ b/typescript/test/testdata/showcase/tsconfig.json.baseline @@ -4,7 +4,10 @@ "rootDir": ".", "outDir": "build", "resolveJsonModule": true, - "lib": ["es2016", "dom"] + "lib": [ + "es2016", + "dom" + ] }, "include": [ "src/*.ts", diff --git a/typescript/test/testdata/texttospeech/package.json.baseline b/typescript/test/testdata/texttospeech/package.json.baseline new file mode 100644 index 000000000..c31b6af3f --- /dev/null +++ b/typescript/test/testdata/texttospeech/package.json.baseline @@ -0,0 +1,30 @@ +{ + "repository": "googleapis/nodejs-texttospeech", + "name": "texttospeech", + "version": "0.1.0", + "author": "Google LLC", + "description": "Texttospeech client for Node.js", + "main": "build/src/index.js", + "dependencies": { + "google-gax": "^1.7.5" + }, + "devDependencies": { + "@types/mocha": "^5.2.5", + "gts": "^0.9.0", + "mocha": "^6.0.0", + "typescript": "~3.5.3" + }, + "scripts": { + "lint": "gts check", + "clean": "gts clean", + "compile": "tsc -p . && cp -r protos build/", + "compile-protos": "compileProtos src", + "fix": "gts fix", + "prepare": "npm run compile-protos && npm run compile", + "test": "mocha build/test" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=8.13.0" + } +} diff --git a/typescript/test/testdata/texttospeech/proto.list.baseline b/typescript/test/testdata/texttospeech/proto.list.baseline new file mode 100644 index 000000000..683a7dcb6 --- /dev/null +++ b/typescript/test/testdata/texttospeech/proto.list.baseline @@ -0,0 +1,6 @@ +google/api/http.proto +google/protobuf/descriptor.proto +google/api/annotations.proto +google/api/client.proto +google/api/field_behavior.proto +google/cloud/texttospeech/v1/cloud_tts.proto diff --git a/typescript/test/testdata/texttospeech/protos/google/cloud/texttospeech/v1/cloud_tts.proto.baseline b/typescript/test/testdata/texttospeech/protos/google/cloud/texttospeech/v1/cloud_tts.proto.baseline new file mode 100644 index 000000000..6263da4ab --- /dev/null +++ b/typescript/test/testdata/texttospeech/protos/google/cloud/texttospeech/v1/cloud_tts.proto.baseline @@ -0,0 +1,253 @@ +// Copyright 2019 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 +// +// http://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. +// + +syntax = "proto3"; + +package google.cloud.texttospeech.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; + +option cc_enable_arenas = true; +option csharp_namespace = "Google.Cloud.TextToSpeech.V1"; +option go_package = "google.golang.org/genproto/googleapis/cloud/texttospeech/v1;texttospeech"; +option java_multiple_files = true; +option java_outer_classname = "TextToSpeechProto"; +option java_package = "com.google.cloud.texttospeech.v1"; +option php_namespace = "Google\\Cloud\\TextToSpeech\\V1"; + +// Service that implements Google Cloud Text-to-Speech API. +service TextToSpeech { + option (google.api.default_host) = "texttospeech.googleapis.com"; + option (google.api.oauth_scopes) = "https://www.googleapis.com/auth/cloud-platform"; + + // Returns a list of Voice supported for synthesis. + rpc ListVoices(ListVoicesRequest) returns (ListVoicesResponse) { + option (google.api.http) = { + get: "/v1/voices" + }; + option (google.api.method_signature) = "language_code"; + } + + // Synthesizes speech synchronously: receive results after all text input + // has been processed. + rpc SynthesizeSpeech(SynthesizeSpeechRequest) returns (SynthesizeSpeechResponse) { + option (google.api.http) = { + post: "/v1/text:synthesize" + body: "*" + }; + option (google.api.method_signature) = "input,voice,audio_config"; + } +} + +// The top-level message sent by the client for the `ListVoices` method. +message ListVoicesRequest { + // Optional. Recommended. + // [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tag. If + // specified, the ListVoices call will only return voices that can be used to + // synthesize this language_code. E.g. when specifying "en-NZ", you will get + // supported "en-*" voices; when specifying "no", you will get supported + // "no-*" (Norwegian) and "nb-*" (Norwegian Bokmal) voices; specifying "zh" + // will also get supported "cmn-*" voices; specifying "zh-hk" will also get + // supported "yue-*" voices. + string language_code = 1 [(google.api.field_behavior) = OPTIONAL]; +} + +// Gender of the voice as described in +// [SSML voice element](https://www.w3.org/TR/speech-synthesis11/#edef_voice). +enum SsmlVoiceGender { + // An unspecified gender. + // In VoiceSelectionParams, this means that the client doesn't care which + // gender the selected voice will have. In the Voice field of + // ListVoicesResponse, this may mean that the voice doesn't fit any of the + // other categories in this enum, or that the gender of the voice isn't known. + SSML_VOICE_GENDER_UNSPECIFIED = 0; + + // A male voice. + MALE = 1; + + // A female voice. + FEMALE = 2; + + // A gender-neutral voice. + NEUTRAL = 3; +} + +// Configuration to set up audio encoder. The encoding determines the output +// audio format that we'd like. +enum AudioEncoding { + // Not specified. Will return result [google.rpc.Code.INVALID_ARGUMENT][]. + AUDIO_ENCODING_UNSPECIFIED = 0; + + // Uncompressed 16-bit signed little-endian samples (Linear PCM). + // Audio content returned as LINEAR16 also contains a WAV header. + LINEAR16 = 1; + + // MP3 audio at 32kbps. + MP3 = 2; + + // Opus encoded audio wrapped in an ogg container. The result will be a + // file which can be played natively on Android, and in browsers (at least + // Chrome and Firefox). The quality of the encoding is considerably higher + // than MP3 while using approximately the same bitrate. + OGG_OPUS = 3; +} + +// The message returned to the client by the `ListVoices` method. +message ListVoicesResponse { + // The list of voices. + repeated Voice voices = 1; +} + +// Description of a voice supported by the TTS service. +message Voice { + // The languages that this voice supports, expressed as + // [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tags (e.g. + // "en-US", "es-419", "cmn-tw"). + repeated string language_codes = 1; + + // The name of this voice. Each distinct voice has a unique name. + string name = 2; + + // The gender of this voice. + SsmlVoiceGender ssml_gender = 3; + + // The natural sample rate (in hertz) for this voice. + int32 natural_sample_rate_hertz = 4; +} + +// The top-level message sent by the client for the `SynthesizeSpeech` method. +message SynthesizeSpeechRequest { + // Required. The Synthesizer requires either plain text or SSML as input. + SynthesisInput input = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The desired voice of the synthesized audio. + VoiceSelectionParams voice = 2 [(google.api.field_behavior) = REQUIRED]; + + // Required. The configuration of the synthesized audio. + AudioConfig audio_config = 3 [(google.api.field_behavior) = REQUIRED]; +} + +// Contains text input to be synthesized. Either `text` or `ssml` must be +// supplied. Supplying both or neither returns +// [google.rpc.Code.INVALID_ARGUMENT][]. The input size is limited to 5000 +// characters. +message SynthesisInput { + // The input source, which is either plain text or SSML. + oneof input_source { + // The raw text to be synthesized. + string text = 1; + + // The SSML document to be synthesized. The SSML document must be valid + // and well-formed. Otherwise the RPC will fail and return + // [google.rpc.Code.INVALID_ARGUMENT][]. For more information, see + // [SSML](/speech/text-to-speech/docs/ssml). + string ssml = 2; + } +} + +// Description of which voice to use for a synthesis request. +message VoiceSelectionParams { + // Required. The language (and potentially also the region) of the voice expressed as a + // [BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tag, e.g. + // "en-US". This should not include a script tag (e.g. use + // "cmn-cn" rather than "cmn-Hant-cn"), because the script will be inferred + // from the input provided in the SynthesisInput. The TTS service + // will use this parameter to help choose an appropriate voice. Note that + // the TTS service may choose a voice with a slightly different language code + // than the one selected; it may substitute a different region + // (e.g. using en-US rather than en-CA if there isn't a Canadian voice + // available), or even a different language, e.g. using "nb" (Norwegian + // Bokmal) instead of "no" (Norwegian)". + string language_code = 1 [(google.api.field_behavior) = REQUIRED]; + + // The name of the voice. If not set, the service will choose a + // voice based on the other parameters such as language_code and gender. + string name = 2; + + // The preferred gender of the voice. If not set, the service will + // choose a voice based on the other parameters such as language_code and + // name. Note that this is only a preference, not requirement; if a + // voice of the appropriate gender is not available, the synthesizer should + // substitute a voice with a different gender rather than failing the request. + SsmlVoiceGender ssml_gender = 3; +} + +// Description of audio data to be synthesized. +message AudioConfig { + // Required. The format of the audio byte stream. + AudioEncoding audio_encoding = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Input only. Speaking rate/speed, in the range [0.25, 4.0]. 1.0 is + // the normal native speed supported by the specific voice. 2.0 is twice as + // fast, and 0.5 is half as fast. If unset(0.0), defaults to the native 1.0 + // speed. Any other values < 0.25 or > 4.0 will return an error. + double speaking_rate = 2 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; + + // Optional. Input only. Speaking pitch, in the range [-20.0, 20.0]. 20 means + // increase 20 semitones from the original pitch. -20 means decrease 20 + // semitones from the original pitch. + double pitch = 3 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; + + // Optional. Input only. Volume gain (in dB) of the normal native volume + // supported by the specific voice, in the range [-96.0, 16.0]. If unset, or + // set to a value of 0.0 (dB), will play at normal native signal amplitude. A + // value of -6.0 (dB) will play at approximately half the amplitude of the + // normal native signal amplitude. A value of +6.0 (dB) will play at + // approximately twice the amplitude of the normal native signal amplitude. + // Strongly recommend not to exceed +10 (dB) as there's usually no effective + // increase in loudness for any value greater than that. + double volume_gain_db = 4 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; + + // Optional. The synthesis sample rate (in hertz) for this audio. When this is + // specified in SynthesizeSpeechRequest, if this is different from the voice's + // natural sample rate, then the synthesizer will honor this request by + // converting to the desired sample rate (which might result in worse audio + // quality), unless the specified sample rate is not supported for the + // encoding chosen, in which case it will fail the request and return + // [google.rpc.Code.INVALID_ARGUMENT][]. + int32 sample_rate_hertz = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Input only. An identifier which selects 'audio effects' profiles + // that are applied on (post synthesized) text to speech. Effects are applied + // on top of each other in the order they are given. See + // [audio + // profiles](https://cloud.google.com/text-to-speech/docs/audio-profiles) for + // current supported profile ids. + repeated string effects_profile_id = 6 [ + (google.api.field_behavior) = INPUT_ONLY, + (google.api.field_behavior) = OPTIONAL + ]; +} + +// The message returned to the client by the `SynthesizeSpeech` method. +message SynthesizeSpeechResponse { + // The audio data bytes encoded as specified in the request, including the + // header for encodings that are wrapped in containers (e.g. MP3, OGG_OPUS). + // For LINEAR16 audio, we include the WAV header. Note: as + // with all bytes fields, protobuffers use a pure binary representation, + // whereas JSON representations use base64. + bytes audio_content = 1; +} diff --git a/typescript/test/testdata/texttospeech/src/index.ts.baseline b/typescript/test/testdata/texttospeech/src/index.ts.baseline new file mode 100644 index 000000000..73a2bd8e7 --- /dev/null +++ b/typescript/test/testdata/texttospeech/src/index.ts.baseline @@ -0,0 +1,22 @@ +// Copyright 2019 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import * as v1 from './v1'; +export {v1}; +const TextToSpeechClient = v1.TextToSpeechClient; +export {TextToSpeechClient}; diff --git a/typescript/test/testdata/texttospeech/src/v1/index.ts.baseline b/typescript/test/testdata/texttospeech/src/v1/index.ts.baseline new file mode 100644 index 000000000..3d58f4c39 --- /dev/null +++ b/typescript/test/testdata/texttospeech/src/v1/index.ts.baseline @@ -0,0 +1,19 @@ +// Copyright 2019 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +export {TextToSpeechClient} from './text_to_speech_client'; diff --git a/typescript/test/testdata/texttospeech/src/v1/license.baseline b/typescript/test/testdata/texttospeech/src/v1/license.baseline new file mode 100644 index 000000000..e69de29bb diff --git a/typescript/test/testdata/texttospeech/src/v1/text_to_speech_client.ts.baseline b/typescript/test/testdata/texttospeech/src/v1/text_to_speech_client.ts.baseline new file mode 100644 index 000000000..6e22a6c2f --- /dev/null +++ b/typescript/test/testdata/texttospeech/src/v1/text_to_speech_client.ts.baseline @@ -0,0 +1,314 @@ +// Copyright 2019 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import * as gax from 'google-gax'; +import * as path from 'path'; + +import * as packageJson from '../../package.json'; +import * as protosTypes from '../../protos/protos'; +import * as gapicConfig from './text_to_speech_client_config.json'; + +const version = packageJson.version; + +export interface ClientOptions extends gax.GrpcClientOptions, + gax.GoogleAuthOptions, + gax.ClientStubOptions { + libName?: string; + libVersion?: string; + clientConfig?: gax.ClientConfig; + fallback?: boolean; + apiEndpoint?: string; +} + +interface Descriptors { + page: {[name: string]: gax.PageDescriptor}; + stream: {[name: string]: gax.StreamDescriptor}; + longrunning: {[name: string]: gax.LongrunningDescriptor}; +} + +export interface Callback< + ResponseObject, NextRequestObject, RawResponseObject> { + (err: Error|null|undefined, value?: ResponseObject|null, + nextRequest?: NextRequestObject, rawResponse?: RawResponseObject): void; +} + + + +export class TextToSpeechClient { + /* + Service that implements Google Cloud Text-to-Speech API. + */ + private _descriptors: Descriptors = {page: {}, stream: {}, longrunning: {}}; + private _innerApiCalls: {[name: string]: Function}; + auth: gax.GoogleAuth; + + /** + * Construct an instance of TextToSpeechClient. + * + * @@param {object} [options] - The configuration object. See the subsequent + * parameters for more details. + * @@param {object} [options.credentials] - Credentials object. + * @@param {string} [options.credentials.client_email] + * @@param {string} [options.credentials.private_key] + * @@param {string} [options.email] - Account email address. Required when + * using a .pem or .p12 keyFilename. + * @@param {string} [options.keyFilename] - Full path to the a .json, .pem, or + * .p12 key downloaded from the Google Developers Console. If you provide + * a path to a JSON file, the projectId option below is not necessary. + * NOTE: .pem and .p12 require you to specify options.email as well. + * @@param {number} [options.port] - The port on which to connect to + * the remote host. + * @@param {string} [options.projectId] - The project ID from the Google + * Developer's Console, e.g. 'grape-spaceship-123'. We will also check + * the environment variable GCLOUD_PROJECT for your project ID. If your + * app is running in an environment which supports + * {@@link https://developers.google.com/identity/protocols/application-default-credentials Application Default Credentials}, + * your project ID will be detected automatically. + * @@param {function} [options.promise] - Custom promise module to use instead + * of native Promises. + * @@param {string} [options.apiEndpoint] - The domain name of the + * API remote host. + */ + + constructor(opts?: ClientOptions) { + // Ensure that options include the service address and port. + const staticMembers = this.constructor as typeof TextToSpeechClient; + const servicePath = opts && opts.servicePath ? + opts.servicePath : + ((opts && opts.apiEndpoint) ? opts.apiEndpoint : + staticMembers.servicePath); + const port = opts && opts.port ? opts.port : staticMembers.port; + + if (!opts) { + opts = {servicePath, port}; + } + opts.servicePath = opts.servicePath || servicePath; + opts.port = opts.port || port; + opts.clientConfig = opts.clientConfig || {}; + + const isBrowser = (typeof window !== 'undefined'); + if (isBrowser){ + opts.fallback = true; + } + // If we are in browser, we are already using fallback because of the + // "browser" field in package.json. + // But if we were explicitly requested to use fallback, let's do it now. + const gaxModule = !isBrowser && opts.fallback ? gax.fallback : gax; + + // Create a `gaxGrpc` object, with any grpc-specific options + // sent to the client. + opts.scopes = (this.constructor as typeof TextToSpeechClient).scopes; + const gaxGrpc = new gaxModule.GrpcClient(opts); + + // Save the auth object to the client, for use by other methods. + this.auth = (gaxGrpc.auth as gax.GoogleAuth); + + // Determine the client header string. + const clientHeader = [ + `gl-node/${process.version}`, + `grpc/${gaxGrpc.grpcVersion}`, + `gax/${gaxModule.version}`, + `gapic/${version}`, + `gl-web/${gaxModule.version}` + ]; + if (opts.libName && opts.libVersion) { + clientHeader.push(`${opts.libName}/${opts.libVersion}`); + } + // Load the applicable protos. + // For Node.js, pass the path to JSON proto file. + // For browsers, pass the JSON content. + + const nodejsProtoPath = path.join(__dirname, '..', '..', 'protos', 'protos.json'); + const protos = gaxGrpc.loadProto( + opts.fallback ? + require("../../protos/protos.json") : + nodejsProtoPath + ); + + // Put together the default options sent with requests. + const defaults = gaxGrpc.constructSettings( + 'google.cloud.texttospeech.v1.TextToSpeech', gapicConfig as gax.ClientConfig, + opts.clientConfig || {}, {'x-goog-api-client': clientHeader.join(' ')}); + + // Set up a dictionary of "inner API calls"; the core implementation + // of calling the API is handled in `google-gax`, with this code + // merely providing the destination and request information. + this._innerApiCalls = {}; + + // Put together the "service stub" for + // google.showcase.v1alpha2.Echo. + const textToSpeechStub = gaxGrpc.createStub( + opts.fallback ? + // @ts-ignore Do not check types for loaded protos + protos.lookupService('google.cloud.texttospeech.v1.TextToSpeech') : + // @ts-ignore Do not check types for loaded protos + protos.google.cloud.texttospeech.v1.TextToSpeech, + opts) as Promise<{[method: string]: Function}>; + + const textToSpeechStubMethods = + ['listVoices', 'synthesizeSpeech']; + + for (const methodName of textToSpeechStubMethods) { + const innerCallPromise = textToSpeechStub.then( + (stub: {[method: string]: Function}) => (...args: Array<{}>) => { + return stub[methodName].apply(stub, args); + }, + (err: Error|null|undefined) => () => { + throw err; + }); + + this._innerApiCalls[methodName] = gax.createApiCall( + innerCallPromise, + defaults[methodName], + this._descriptors.page[methodName] || + this._descriptors.stream[methodName] || + this._descriptors.longrunning[methodName] + ); + } + } + /** + * The DNS address for this API service. + */ + static get servicePath() { + return 'texttospeech.googleapis.com'; + } + /** + * The DNS address for this API service - same as servicePath(), + * exists for compatibility reasons. + */ + static get apiEndpoint() { + return 'texttospeech.googleapis.com'; + } + + /** + * The port for this API service. + */ + static get port() { + return 443; + } + + /** + * The scopes needed to make gRPC calls for every method defined + * in this service. + */ + static get scopes() { + return [ + 'https://www.googleapis.com/auth/cloud-platform' + ]; + } + + /** + * Return the project ID used by this class. + * @param {function(Error, string)} callback - the callback to + * be called with the current project Id. + */ + getProjectId(): Promise; + getProjectId(callback: Callback): void; + getProjectId(callback?: Callback): + Promise|void { + if (callback) { + this.auth.getProjectId(callback); + return; + } + return this.auth.getProjectId(); + } + + // ------------------- + // -- Service calls -- + // ------------------- + /* + Returns a list of Voice supported for synthesis. + */ + listVoices( + request: protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest, + options?: gax.CallOptions): + Promise<[ + protosTypes.google.cloud.texttospeech.v1.IListVoicesResponse, + protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest|undefined, {}|undefined + ]>; + listVoices( + request: protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest, + options: gax.CallOptions, + callback: Callback< + protosTypes.google.cloud.texttospeech.v1.IListVoicesResponse, + protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest|undefined, + {}|undefined>): void; + listVoices( + request: protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest, + optionsOrCallback?: gax.CallOptions|Callback< + protosTypes.google.cloud.texttospeech.v1.IListVoicesResponse, + protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest|undefined, {}|undefined>, + callback?: Callback< + protosTypes.google.cloud.texttospeech.v1.IListVoicesResponse, + protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest|undefined, + {}|undefined>): + Promise<[ + protosTypes.google.cloud.texttospeech.v1.IListVoicesResponse, + protosTypes.google.cloud.texttospeech.v1.IListVoicesRequest|undefined, {}|undefined + ]>|void { + request = request || {}; + let options = optionsOrCallback; + if (typeof options === 'function' && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; + return this._innerApiCalls.listVoices(request, options, callback); + } + /* + Synthesizes speech synchronously: receive results after all text input + has been processed. + */ + synthesizeSpeech( + request: protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest, + options?: gax.CallOptions): + Promise<[ + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechResponse, + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest|undefined, {}|undefined + ]>; + synthesizeSpeech( + request: protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest, + options: gax.CallOptions, + callback: Callback< + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechResponse, + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest|undefined, + {}|undefined>): void; + synthesizeSpeech( + request: protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest, + optionsOrCallback?: gax.CallOptions|Callback< + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechResponse, + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest|undefined, {}|undefined>, + callback?: Callback< + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechResponse, + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest|undefined, + {}|undefined>): + Promise<[ + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechResponse, + protosTypes.google.cloud.texttospeech.v1.ISynthesizeSpeechRequest|undefined, {}|undefined + ]>|void { + request = request || {}; + let options = optionsOrCallback; + if (typeof options === 'function' && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; + return this._innerApiCalls.synthesizeSpeech(request, options, callback); + } + +} diff --git a/typescript/test/testdata/texttospeech/src/v1/text_to_speech_client_config.json.baseline b/typescript/test/testdata/texttospeech/src/v1/text_to_speech_client_config.json.baseline new file mode 100644 index 000000000..bd939ce35 --- /dev/null +++ b/typescript/test/testdata/texttospeech/src/v1/text_to_speech_client_config.json.baseline @@ -0,0 +1,36 @@ +{ + "interfaces": { + "google.cloud.texttospeech.v1.TextToSpeech": { + "retry_codes": { + "non_idempotent": [], + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ] + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 20000, + "rpc_timeout_multiplier": 1, + "max_rpc_timeout_millis": 20000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListVoices": { + "timeout_millis": 600000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SynthesizeSpeech": { + "timeout_millis": 600000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/typescript/test/testdata/texttospeech/src/v1/text_to_speech_proto_list.json.baseline b/typescript/test/testdata/texttospeech/src/v1/text_to_speech_proto_list.json.baseline new file mode 100644 index 000000000..a2b3f234f --- /dev/null +++ b/typescript/test/testdata/texttospeech/src/v1/text_to_speech_proto_list.json.baseline @@ -0,0 +1,3 @@ +[ + "../../protos/google/cloud/texttospeech/v1/cloud_tts.proto" +] diff --git a/typescript/test/testdata/texttospeech/test/gapic-text_to_speech-v1.ts.baseline b/typescript/test/testdata/texttospeech/test/gapic-text_to_speech-v1.ts.baseline new file mode 100644 index 000000000..959772ecd --- /dev/null +++ b/typescript/test/testdata/texttospeech/test/gapic-text_to_speech-v1.ts.baseline @@ -0,0 +1,170 @@ +// Copyright 2019 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +'use strict'; + +const assert = require('assert'); +const TextToSpeechModule = require('../src'); + + +const FAKE_STATUS_CODE = 1; +class FakeError{ + code: number; + constructor(n: number){ + this.code = n; + } +} +const error = new FakeError(FAKE_STATUS_CODE); +export interface Callback { + (err: FakeError|null, response?: {} | null): {}; +} + +export class Operation{ + constructor(){}; + promise() {() => {}}; +} + +function mockSimpleGrpcMethod(expectedRequest: {}, response: {} | null, error: FakeError | null) { + return function(actualRequest: {}, options: {}, callback: Callback) { + assert.deepStrictEqual(actualRequest, expectedRequest); + if (error) { + callback(error); + } else if (response) { + callback(null, response); + } else { + callback(null); + } + }; +} +describe('TextToSpeechClient', () => { + it('has servicePath', () => { + const servicePath = TextToSpeechModule.v1.TextToSpeechClient.servicePath; + assert(servicePath); + }); + it('has apiEndpoint', () => { + const apiEndpoint = TextToSpeechModule.v1.TextToSpeechClient.apiEndpoint; + assert(apiEndpoint); + }); + it('has port', () => { + const port = TextToSpeechModule.v1.TextToSpeechClient.port; + assert(port); + assert(typeof port === 'number'); + }); + it('should create a client with no option', () => { + const client = new TextToSpeechModule.v1.TextToSpeechClient(); + assert(client); + }); + it('should create a client with gRPC option', () => { + const client = new TextToSpeechModule.v1.TextToSpeechClient({ + fallback: true, + }); + assert(client); + }); + describe('listVoices', () => { + it('invokes listVoices without error', done => { + const client = new TextToSpeechModule.v1.TextToSpeechClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + // Mock request + const request = {}; + // Mock response + const expectedResponse = {}; + // Mock gRPC layer + client._innerApiCalls.listVoices = mockSimpleGrpcMethod( + request, + expectedResponse, + null + ); + client.listVoices(request, (err: {}, response: {}) => { + assert.ifError(err); + assert.deepStrictEqual(response, expectedResponse); + done(); + }) + }); + + it('invokes listVoices with error', done => { + const client = new TextToSpeechModule.v1.TextToSpeechClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + // Mock request + const request = {}; + // Mock response + const expectedResponse = {}; + // Mock gRPC layer + client._innerApiCalls.listVoices = mockSimpleGrpcMethod( + request, + null, + error + ); + client.listVoices(request, (err: FakeError, response: {}) => { + assert(err instanceof FakeError); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + assert(typeof response === 'undefined'); + done(); + }) + }); + }); + describe('synthesizeSpeech', () => { + it('invokes synthesizeSpeech without error', done => { + const client = new TextToSpeechModule.v1.TextToSpeechClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + // Mock request + const request = {}; + // Mock response + const expectedResponse = {}; + // Mock gRPC layer + client._innerApiCalls.synthesizeSpeech = mockSimpleGrpcMethod( + request, + expectedResponse, + null + ); + client.synthesizeSpeech(request, (err: {}, response: {}) => { + assert.ifError(err); + assert.deepStrictEqual(response, expectedResponse); + done(); + }) + }); + + it('invokes synthesizeSpeech with error', done => { + const client = new TextToSpeechModule.v1.TextToSpeechClient({ + credentials: {client_email: 'bogus', private_key: 'bogus'}, + projectId: 'bogus', + }); + // Mock request + const request = {}; + // Mock response + const expectedResponse = {}; + // Mock gRPC layer + client._innerApiCalls.synthesizeSpeech = mockSimpleGrpcMethod( + request, + null, + error + ); + client.synthesizeSpeech(request, (err: FakeError, response: {}) => { + assert(err instanceof FakeError); + assert.strictEqual(err.code, FAKE_STATUS_CODE); + assert(typeof response === 'undefined'); + done(); + }) + }); + }); +}); diff --git a/typescript/test/testdata/texttospeech/tsconfig.json.baseline b/typescript/test/testdata/texttospeech/tsconfig.json.baseline new file mode 100644 index 000000000..653c21079 --- /dev/null +++ b/typescript/test/testdata/texttospeech/tsconfig.json.baseline @@ -0,0 +1,18 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build", + "resolveJsonModule": true, + "lib": [ + "es2016", + "dom" + ] + }, + "include": [ + "src/*.ts", + "src/**/*.ts", + "test/*.ts", + "test/**/*.ts" + ] +} diff --git a/typescript/test/testdata/texttospeech/tslint.json.baseline b/typescript/test/testdata/texttospeech/tslint.json.baseline new file mode 100644 index 000000000..b3bfaf592 --- /dev/null +++ b/typescript/test/testdata/texttospeech/tslint.json.baseline @@ -0,0 +1,6 @@ +{ + "extends": "gts/tslint.json", + "rules": { + "ban-ts-ignore": false + } +} diff --git a/typescript/test/unit/codemap.ts b/typescript/test/unit/codemap.ts new file mode 100644 index 000000000..001a243a9 --- /dev/null +++ b/typescript/test/unit/codemap.ts @@ -0,0 +1,159 @@ +import { RetryableCodeMap } from '../../src/schema/proto'; +import * as plugin from '../../../pbjs-genfiles/plugin'; +import * as assert from 'assert'; + +const Code = plugin.google.rpc.Code; + +describe('RetryableCodeMap', () => { + describe('Retry codes', () => { + it('has readable names for common code lists', () => { + const map = new RetryableCodeMap(); + assert.strictEqual(map.getRetryableCodesName([]), 'non_idempotent'); + assert.strictEqual( + map.getRetryableCodesName([Code.UNAVAILABLE, Code.DEADLINE_EXCEEDED]), + 'idempotent' + ); + }); + + it('generates the same readable name for the same set of parameters', () => { + const map = new RetryableCodeMap(); + const name1 = map.getRetryableCodesName([ + Code.FAILED_PRECONDITION, + Code.ALREADY_EXISTS, + Code.INVALID_ARGUMENT, + ]); + const name2 = map.getRetryableCodesName([ + Code.INVALID_ARGUMENT, + Code.ALREADY_EXISTS, + Code.FAILED_PRECONDITION, + ]); + assert.strictEqual(name1, name2); + assert.notStrictEqual(name1, 'idempotent'); + assert.notStrictEqual(name1, 'non_idempotent'); + assert.notStrictEqual(name2, 'idempotent'); + assert.notStrictEqual(name2, 'non_idempotent'); + }); + + it('generates different readable names for different sets of parameters', () => { + const map = new RetryableCodeMap(); + const name1 = map.getRetryableCodesName([ + Code.FAILED_PRECONDITION, + Code.ALREADY_EXISTS, + Code.INVALID_ARGUMENT, + ]); + const name2 = map.getRetryableCodesName([ + Code.INVALID_ARGUMENT, + Code.FAILED_PRECONDITION, + ]); + assert.notStrictEqual(name1, name2); + assert.notStrictEqual(name1, 'idempotent'); + assert.notStrictEqual(name1, 'non_idempotent'); + assert.notStrictEqual(name2, 'idempotent'); + assert.notStrictEqual(name2, 'non_idempotent'); + }); + + it('returns list of all readable names', () => { + const map = new RetryableCodeMap(); + const name1 = map.getRetryableCodesName([Code.ABORTED]); + const name2 = map.getRetryableCodesName([Code.ALREADY_EXISTS]); + const names = map.getPrettyCodesNames(); + assert.strictEqual(names.length, 4); + assert.notStrictEqual(name1, name2); + assert.notStrictEqual(name1, 'idempotent'); + assert.notStrictEqual(name1, 'non_idempotent'); + assert.notStrictEqual(name2, 'idempotent'); + assert.notStrictEqual(name2, 'non_idempotent'); + assert(names.includes('idempotent')); + assert(names.includes('non_idempotent')); + assert(names.includes(name1)); + assert(names.includes(name2)); + }); + + it('allows to suggest a name', () => { + const map = new RetryableCodeMap(); + const name = map.getRetryableCodesName([Code.ABORTED], 'suggested_name'); + assert.strictEqual(name, 'suggested_name'); + }); + + it('returns valid JSON list of codes by name', () => { + const map = new RetryableCodeMap(); + const name = map.getRetryableCodesName([ + Code.FAILED_PRECONDITION, + Code.ALREADY_EXISTS, + Code.INVALID_ARGUMENT, + ]); + const jsonString = map.getCodesJSON(name); + const json = JSON.parse(jsonString); + assert(Array.isArray(json)); + assert.strictEqual(json.length, 3); + assert(json.includes('FAILED_PRECONDITION')); + assert(json.includes('ALREADY_EXISTS')); + assert(json.includes('INVALID_ARGUMENT')); + }); + }); + + describe('Retry options', () => { + it('has a readable name for default retry options', () => { + const map = new RetryableCodeMap(); + assert.strictEqual( + map.getParamsName({ + initial_retry_delay_millis: 100, + retry_delay_multiplier: 1.3, + max_retry_delay_millis: 60000, + initial_rpc_timeout_millis: 20000, + rpc_timeout_multiplier: 1.0, + max_rpc_timeout_millis: 20000, + total_timeout_millis: 600000, + }), + 'default' + ); + }); + + it('generates the same name for the same set of options', () => { + const map = new RetryableCodeMap(); + const name1 = map.getParamsName({ a: 10, b: 20 }); + const name2 = map.getParamsName({ b: 20.0, a: 10.0 }); + assert.strictEqual(name1, name2); + assert.notStrictEqual(name1, 'default'); + assert.notStrictEqual(name2, 'default'); + }); + + it('generates different names for different sets of parameters', () => { + const map = new RetryableCodeMap(); + const name1 = map.getParamsName({ a: 1 }); + const name2 = map.getParamsName({ a: 2 }); + assert.notStrictEqual(name1, name2); + assert.notStrictEqual(name1, 'default'); + assert.notStrictEqual(name2, 'default'); + }); + + it('returns list of all names', () => { + const map = new RetryableCodeMap(); + const name1 = map.getParamsName({ a: 1 }); + const name2 = map.getParamsName({ a: 2 }); + const names = map.getPrettyParamsNames(); + assert.strictEqual(names.length, 3); + assert.notStrictEqual(name1, name2); + assert.notStrictEqual(name1, 'default'); + assert.notStrictEqual(name2, 'default'); + assert(names.includes('default')); + assert(names.includes(name1)); + assert(names.includes(name2)); + }); + + it('allows to suggest a name', () => { + const map = new RetryableCodeMap(); + const name = map.getParamsName({ a: 1 }, 'suggested_name'); + assert.strictEqual(name, 'suggested_name'); + }); + + it('returns valid JSON object of parameters by name', () => { + const map = new RetryableCodeMap(); + const param = { a: 1, b: 2 }; + const name = map.getParamsName(param); + const jsonString = map.getParamsJSON(name); + const json = JSON.parse(jsonString); + assert.deepStrictEqual(json, param); + }); + }); +}); diff --git a/typescript/test/unit/grpc-client-config.ts b/typescript/test/unit/grpc-client-config.ts new file mode 100644 index 000000000..350a9662b --- /dev/null +++ b/typescript/test/unit/grpc-client-config.ts @@ -0,0 +1,90 @@ +// Copyright 2019 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. + +import * as assert from 'assert'; +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; +import { equalToBaseline } from '../util'; + +const cwd = process.cwd(); + +const OUTPUT_DIR = path.join(cwd, '.test-out-texttospeech'); +const GOOGLE_GAX_PROTOS_DIR = path.join( + cwd, + 'node_modules', + 'google-gax', + 'protos' +); +const PROTOS_DIR = path.join(cwd, 'build', 'test', 'protos'); +const TTS_PROTO_FILE = path.join( + PROTOS_DIR, + 'google', + 'cloud', + 'texttospeech', + 'v1', + 'cloud_tts.proto' +); + +const GRPC_SERVICE_CONFIG = path.join( + PROTOS_DIR, + 'google', + 'cloud', + 'texttospeech', + 'v1', + 'texttospeech_grpc_service_config.json' +); + +const BASELINE_DIR = path.join( + __dirname, + '..', + '..', + '..', + 'typescript', + 'test', + 'testdata', + 'texttospeech' +); + +const SRCDIR = path.join(cwd, 'build', 'src'); +const CLI = path.join(SRCDIR, 'cli.js'); + +describe('gRPC Client Config', () => { + describe('Generate Text-to-Speech library', () => { + it('Generated proto list should have same output with baseline.', function() { + this.timeout(10000); + if (fs.existsSync(OUTPUT_DIR)) { + rimraf.sync(OUTPUT_DIR); + } + fs.mkdirSync(OUTPUT_DIR); + + try { + execSync(`chmod +x ${CLI}`); + } catch (err) { + console.warn(`Failed to chmod +x ${CLI}: ${err}. Ignoring...`); + } + + execSync( + `node build/src/start_script.js ` + + `--output-dir=${OUTPUT_DIR} ` + + `-I ${GOOGLE_GAX_PROTOS_DIR} ` + + `-I ${PROTOS_DIR} ` + + `--grpc-service-config=${GRPC_SERVICE_CONFIG} ` + + TTS_PROTO_FILE + ); + assert(equalToBaseline(OUTPUT_DIR, BASELINE_DIR)); + }); + }); +}); diff --git a/typescript/test/unit/util.ts b/typescript/test/unit/util.ts index 1641ef693..e4466d50e 100644 --- a/typescript/test/unit/util.ts +++ b/typescript/test/unit/util.ts @@ -13,7 +13,8 @@ // limitations under the License. import * as assert from 'assert'; -import { commonPrefix } from '../../src/util'; +import { commonPrefix, duration, seconds, milliseconds } from '../../src/util'; +import * as plugin from '../../../pbjs-genfiles/plugin'; describe('util.ts', () => { describe('CommonPrefix', () => { @@ -31,6 +32,86 @@ describe('util.ts', () => { }); }); + describe('Duration', () => { + it('should support fractional seconds', () => { + const input = '0.1s'; + const dur = duration(input); + assert.strictEqual(Number(dur.seconds), 0); + assert.strictEqual(Number(dur.nanos), 0.1 * 1e9); + }); + + it('should support fractional minutes', () => { + const input = '0.5m'; + const dur = duration(input); + assert.strictEqual(Number(dur.seconds), 30); + assert.strictEqual(Number(dur.nanos), 0); + }); + + it('should build correct Duration object for seconds', () => { + const input = '5s'; + const dur = duration(input); + assert.strictEqual(Number(dur.seconds), 5); + assert.strictEqual(Number(dur.nanos), 0); + }); + + it('should build correct Duration object for minutes', () => { + const input = '10m'; + const dur = duration(input); + assert.strictEqual(Number(dur.seconds), 10 * 60); + assert.strictEqual(Number(dur.nanos), 0); + }); + + it('should build correct Duration object for hours', () => { + const input = '2h'; + const dur = duration(input); + assert.strictEqual(Number(dur.seconds), 2 * 60 * 60); + assert.strictEqual(Number(dur.nanos), 0); + }); + + it('should build correct Duration object for days', () => { + const input = '3d'; + const dur = duration(input); + assert.strictEqual(Number(dur.seconds), 3 * 60 * 60 * 24); + assert.strictEqual(Number(dur.nanos), 0); + }); + + it('should convert Duration to whole seconds', () => { + const duration = plugin.google.protobuf.Duration.fromObject({ + seconds: 10, + nanos: 0, + }); + const result = seconds(duration); + assert.strictEqual(result, 10); + }); + + it('should convert Duration to fractional seconds', () => { + const duration = plugin.google.protobuf.Duration.fromObject({ + seconds: 5, + nanos: 500000000, + }); + const result = seconds(duration); + assert.strictEqual(result, 5.5); + }); + + it('should convert Duration to whole milliseconds', () => { + const duration = plugin.google.protobuf.Duration.fromObject({ + seconds: 10, + nanos: 0, + }); + const result = milliseconds(duration); + assert.strictEqual(result, 10000); + }); + + it('should convert Duration to fractional milliseconds', () => { + const duration = plugin.google.protobuf.Duration.fromObject({ + seconds: 5, + nanos: 500000000, + }); + const result = milliseconds(duration); + assert.strictEqual(result, 5500); + }); + }); + describe('String manipulation', () => { it('should capitalize', () => { assert.strictEqual(''.capitalize(), '');