Skip to content

Commit

Permalink
fix: protobufjs-compatible camel case (#1286)
Browse files Browse the repository at this point in the history
* fix: protobufjs-compatible camel case

* fix: actually fix the usage

* fix: formatting
  • Loading branch information
alexander-fenster authored Jul 1, 2022
1 parent 93e4ebe commit c751200
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/bundlingCalls/bundleDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {NormalApiCaller} from '../normalCalls/normalApiCaller';

import {BundleApiCaller} from './bundleApiCaller';
import {BundleExecutor} from './bundleExecutor';
import {snakeToCamelCase} from '../util';
import {toCamelCase as snakeToCamelCase} from '../util';

/**
* A descriptor for calls that can be bundled into one call.
Expand Down
8 changes: 4 additions & 4 deletions src/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* limitations under the License.
*/

import {OutgoingHttpHeaders} from 'http';
import * as objectHash from 'object-hash';
import * as protobuf from 'protobufjs';
import * as gax from './gax';
import * as routingHeader from './routingHeader';
import {Status} from './status';
import {OutgoingHttpHeaders} from 'http';
import {
GoogleAuth,
OAuth2Client,
Expand All @@ -39,7 +40,7 @@ import * as fallbackRest from './fallbackRest';
import {isNodeJS} from './featureDetection';
import {generateServiceStub} from './fallbackServiceStub';
import {StreamType} from './streamingCalls/streaming';
import * as objectHash from 'object-hash';
import {toLowerCamelCase} from './util';
import {google} from '../protos/http';

export {FallbackServiceError};
Expand Down Expand Up @@ -151,8 +152,7 @@ export class GrpcClient {
private static getServiceMethods(service: protobuf.Service): ServiceMethods {
const methods: {[name: string]: protobuf.Method} = {};
for (const [methodName, methodObject] of Object.entries(service.methods)) {
const methodNameLowerCamelCase =
methodName[0].toLowerCase() + methodName.substring(1);
const methodNameLowerCamelCase = toLowerCamelCase(methodName);
methods[methodNameLowerCamelCase] = methodObject;
}

Expand Down
3 changes: 2 additions & 1 deletion src/gax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

import {BundleOptions} from './bundlingCalls/bundleExecutor';
import {toLowerCamelCase} from './util';

/**
* Encapsulates the overridable settings for a particular API call.
Expand Down Expand Up @@ -657,7 +658,7 @@ export function constructSettings(
const overridingMethods = overrides.methods || {};
for (const methodName in methods) {
const methodConfig = methods[methodName];
const jsName = methodName[0].toLowerCase() + methodName.slice(1);
const jsName = toLowerCamelCase(methodName);

let retry = constructRetry(
methodConfig,
Expand Down
2 changes: 1 addition & 1 deletion src/transcoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import {JSONObject, JSONValue} from 'proto3-json-serializer';
import {Field} from 'protobufjs';
import {google} from '../protos/http';
import {camelToSnakeCase, snakeToCamelCase} from './util';
import {camelToSnakeCase, toCamelCase as snakeToCamelCase} from './util';

export interface TranscodedRequest {
httpMethod: 'get' | 'post' | 'put' | 'patch' | 'delete';
Expand Down
78 changes: 66 additions & 12 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,50 @@
* limitations under the License.
*/

function words(str: string, normalize = false) {
if (normalize) {
// strings like somethingABCSomething are special case for protobuf.js,
// they should be split as "something", "abc", "something".
// Deal with sequences of capital letters first.
str = str.replace(/([A-Z])([A-Z]+)([A-Z])/g, (str: string) => {
return (
str[0] +
str.slice(1, str.length - 1).toLowerCase() +
str[str.length - 1]
);
});
}
// split on spaces, non-alphanumeric, or capital letters
// note: we keep the capitalization of the first word (special case: IPProtocol)
return str
.split(/(?=[A-Z])|[^A-Za-z0-9.]+/)
.filter(w => w.length > 0)
.map((w, index) => (index === 0 ? w : w.toLowerCase()));
}

/**
* Converts the first character of the given string to lower case.
*/
function lowercase(str: string) {
if (str.length === 0) {
return str;
}
return str[0].toLowerCase() + str.slice(1);
}

/**
* Converts a given string from camelCase (used by protobuf.js and in JSON)
* to snake_case (normally used in proto definitions).
*/
export function camelToSnakeCase(str: string) {
// Keep the first position capitalization, otherwise decapitalize with underscore.
return str.replace(/(?!^)[A-Z]/g, letter => `_${letter.toLowerCase()}`);
const wordsList = words(str);
if (wordsList.length === 0) {
return str;
}
const result = [wordsList[0]];
result.push(...wordsList.slice(1).map(lowercase));
return result.join('_');
}

/**
Expand All @@ -34,18 +71,35 @@ function capitalize(str: string) {
}

/**
* Converts a given string from snake_case (normally used in proto definitions) to
* camelCase (used by protobuf.js)
* Converts a given string from snake_case (normally used in proto definitions) or
* PascalCase (also used in proto definitions) to camelCase (used by protobuf.js).
* Preserves capitalization of the first character.
*/
export function snakeToCamelCase(str: string) {
// split on spaces, underscore, or capital letters
const splitted = str
.split(/(?=[A-Z])|(?:(?!(_(\W+)))[\s_])+/)
.filter(w => w && w.length > 0)
// Keep the capitalization for the first split.
.map((word, index) => (index === 0 ? word : word.toLowerCase()));
if (splitted.length === 0) {
export function toCamelCase(str: string) {
const wordsList = words(str, /*normalize:*/ true);
if (wordsList.length === 0) {
return str;
}
return [splitted[0], ...splitted.slice(1).map(capitalize)].join('');
const result = [wordsList[0]];
result.push(
...wordsList.slice(1).map(w => {
if (w.match(/^\d+$/)) {
return '_' + w;
}
return capitalize(w);
})
);
return result.join('');
}

/**
* Converts a given string to lower camel case (forcing the first character to be
* in lower case).
*/
export function toLowerCamelCase(str: string) {
const camelCase = toCamelCase(str);
if (camelCase.length === 0) {
return camelCase;
}
return camelCase[0].toLowerCase() + camelCase.slice(1);
}
10 changes: 5 additions & 5 deletions test/fixtures/google-gax-packaging-test-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ async function testEchoError(client: EchoClient) {
Error);
try {
await client.echo(request);
} catch (err: any) {
} catch (err) {
clearTimeout(timer);
assert.strictEqual(JSON.stringify(err.statusDetails), JSON.stringify(expectedDetails));
assert.strictEqual(JSON.stringify((err as typeof GoogleError).statusDetails), JSON.stringify(expectedDetails));
assert.ok(errorInfo!)
assert.strictEqual(err.domain, errorInfo!.domain)
assert.strictEqual(err.reason, errorInfo!.reason)
assert.strictEqual(JSON.stringify(err.errorInfoMetadata), JSON.stringify(errorInfo!.metadata));
assert.strictEqual((err as typeof GoogleError).domain, errorInfo!.domain)
assert.strictEqual((err as typeof GoogleError).reason, errorInfo!.reason)
assert.strictEqual(JSON.stringify((err as typeof GoogleError).errorInfoMetadata), JSON.stringify(errorInfo!.metadata));
}
}

Expand Down
5 changes: 4 additions & 1 deletion test/unit/transcoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import {
overrideHttpRules,
} from '../../src/transcoding';
import * as assert from 'assert';
import {camelToSnakeCase, snakeToCamelCase} from '../../src/util';
import {
camelToSnakeCase,
toCamelCase as snakeToCamelCase,
} from '../../src/util';
import * as protobuf from 'protobufjs';
import {testMessageJson} from '../fixtures/fallbackOptional';
import {echoProtoJson} from '../fixtures/echoProtoJson';
Expand Down
48 changes: 39 additions & 9 deletions test/unit/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

import * as assert from 'assert';
import {describe, it} from 'mocha';
import {snakeToCamelCase, camelToSnakeCase} from '../../src/util';
import {
toCamelCase as snakeToCamelCase,
camelToSnakeCase,
toLowerCamelCase,
} from '../../src/util';

describe('util.ts', () => {
it('camelToSnakeCase', () => {
Expand All @@ -29,10 +33,11 @@ describe('util.ts', () => {
assert.strictEqual(camelToSnakeCase('a.1'), 'a.1');
assert.strictEqual(camelToSnakeCase('abc.1Foo'), 'abc.1_foo');
assert.strictEqual(camelToSnakeCase('abc.foo'), 'abc.foo');
assert.strictEqual(camelToSnakeCase('a!.\\'), 'a!.\\');
assert.strictEqual(camelToSnakeCase('!._\\`'), '!._\\`');
assert.strictEqual(camelToSnakeCase('a!B`'), 'a!_b`');
assert.strictEqual(camelToSnakeCase('a.1B`'), 'a.1_b`');
assert.strictEqual(camelToSnakeCase('a.1B'), 'a.1_b');
assert.strictEqual(
camelToSnakeCase('somethingABCDEValue`'),
'something_a_b_c_d_e_value'
);
});

it('snakeToCamelCase', () => {
Expand All @@ -44,9 +49,34 @@ describe('util.ts', () => {
assert.strictEqual(snakeToCamelCase('a.1'), 'a.1');
assert.strictEqual(snakeToCamelCase('abc.1_foo'), 'abc.1Foo');
assert.strictEqual(snakeToCamelCase('abc.foo'), 'abc.foo');
assert.strictEqual(snakeToCamelCase('a!.\\`'), 'a!.\\`');
assert.strictEqual(snakeToCamelCase('!._\\`'), '!._\\`');
assert.strictEqual(snakeToCamelCase('a!_b`'), 'a!B`');
assert.strictEqual(snakeToCamelCase('a.1_b`'), 'a.1B`');
assert.strictEqual(snakeToCamelCase('a.1_b'), 'a.1B');
assert.strictEqual(
snakeToCamelCase('something_abcde_value'),
'somethingAbcdeValue'
);
});

it('toLowerCamelCase', () => {
assert.strictEqual(toLowerCamelCase('test'), 'test');
assert.strictEqual(toLowerCamelCase('test123'), 'test123');
assert.strictEqual(toLowerCamelCase('test_abc'), 'testAbc');
assert.strictEqual(toLowerCamelCase('test_abc_def'), 'testAbcDef');
assert.strictEqual(toLowerCamelCase('I_p_protocol'), 'iPProtocol');
assert.strictEqual(toLowerCamelCase('a.1'), 'a.1');
assert.strictEqual(toLowerCamelCase('abc.1_foo'), 'abc.1Foo');
assert.strictEqual(toLowerCamelCase('abc.foo'), 'abc.foo');
assert.strictEqual(toLowerCamelCase('a.1_b'), 'a.1B');
assert.strictEqual(
toLowerCamelCase('something_abcde_value'),
'somethingAbcdeValue'
);
assert.strictEqual(
toLowerCamelCase('PascalCaseString'),
'pascalCaseString'
);
assert.strictEqual(
toLowerCamelCase('PascalCASEString'),
'pascalCaseString'
);
});
});

0 comments on commit c751200

Please sign in to comment.