Repository: stephenh/ts-proto
Pull request #443 (2021-12-13 17:23:06)

chore: Tweak option handling


Patch:
diff --git a/integration/simple-snake/simple.ts b/integration/simple-snake/simple.ts
index b71b7b6a..6fd7a588 100644
--- a/integration/simple-snake/simple.ts
+++ b/integration/simple-snake/simple.ts
@@ -894,21 +894,21 @@ export const SimpleWithMap = {
 
   fromJSON(object: any): SimpleWithMap {
     const message = { ...baseSimpleWithMap } as SimpleWithMap;
-    message.entitiesById = Object.entries(object.entities_by_id ?? {}).reduce<{ [key: number]: Entity }>(
+    message.entitiesById = Object.entries(object.entitiesById ?? {}).reduce<{ [key: number]: Entity }>(
       (acc, [key, value]) => {
         acc[Number(key)] = Entity.fromJSON(value);
         return acc;
       },
       {}
     );
-    message.nameLookup = Object.entries(object.name_lookup ?? {}).reduce<{ [key: string]: string }>(
+    message.nameLookup = Object.entries(object.nameLookup ?? {}).reduce<{ [key: string]: string }>(
       (acc, [key, value]) => {
         acc[key] = String(value);
         return acc;
       },
       {}
     );
-    message.intLookup = Object.entries(object.int_lookup ?? {}).reduce<{ [key: number]: number }>(
+    message.intLookup = Object.entries(object.intLookup ?? {}).reduce<{ [key: number]: number }>(
       (acc, [key, value]) => {
         acc[Number(key)] = Number(value);
         return acc;
@@ -920,22 +920,22 @@ export const SimpleWithMap = {
 
   toJSON(message: SimpleWithMap): unknown {
     const obj: any = {};
-    obj.entities_by_id = {};
+    obj.entitiesById = {};
     if (message.entitiesById) {
       Object.entries(message.entitiesById).forEach(([k, v]) => {
-        obj.entities_by_id[k] = Entity.toJSON(v);
+        obj.entitiesById[k] = Entity.toJSON(v);
       });
     }
-    obj.name_lookup = {};
+    obj.nameLookup = {};
     if (message.nameLookup) {
       Object.entries(message.nameLookup).forEach(([k, v]) => {
-        obj.name_lookup[k] = v;
+        obj.nameLookup[k] = v;
       });
     }
-    obj.int_lookup = {};
+    obj.intLookup = {};
     if (message.intLookup) {
       Object.entries(message.intLookup).forEach(([k, v]) => {
-        obj.int_lookup[k] = v;
+        obj.intLookup[k] = v;
       });
     }
     return obj;
diff --git a/src/case.ts b/src/case.ts
index 88a4b948..5e57b5fa 100644
--- a/src/case.ts
+++ b/src/case.ts
@@ -1,9 +1,7 @@
 import { Options } from './options';
 
 export function maybeSnakeToCamel(s: string, options: Pick<Options, 'snakeToCamel'>): string {
-  const fromStringSnakeToCamel = typeof options.snakeToCamel === 'string' && options.snakeToCamel.includes('keys');
-  const fromBooleanSnakeToCamel = options.snakeToCamel === true;
-  if ((fromBooleanSnakeToCamel || fromStringSnakeToCamel) && s.includes('_')) {
+  if (options.snakeToCamel.includes('keys') && s.includes('_')) {
     return s
       .split('_')
       .map((word, i) => {
@@ -20,10 +18,6 @@ export function maybeSnakeToCamel(s: string, options: Pick<Options, 'snakeToCame
   }
 }
 
-export function stringToSnakeCase(s: string) {
-  return s.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
-}
-
 export function camelToSnake(s: string): string {
   return s
     .replace(/[\w]([A-Z])/g, function (m) {
diff --git a/src/options.ts b/src/options.ts
index cd464b4b..54a4367e 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -30,7 +30,7 @@ export enum ServiceOption {
 
 export type Options = {
   context: boolean;
-  snakeToCamel: boolean | string;
+  snakeToCamel: Array<'json' | 'keys'>;
   forceLong: LongOption;
   useOptionals: boolean;
   useDate: DateOption;
@@ -62,7 +62,7 @@ export type Options = {
 export function defaultOptions(): Options {
   return {
     context: false,
-    snakeToCamel: true,
+    snakeToCamel: ['json', 'keys'],
     forceLong: LongOption.NUMBER,
     useOptionals: false,
     useDate: DateOption.DATE,
@@ -100,7 +100,7 @@ const nestJsOptions: Partial<Options> = {
   useDate: DateOption.TIMESTAMP,
 };
 
-export function optionsFromParameter(parameter: string): Options {
+export function optionsFromParameter(parameter: string | undefined): Options {
   const options = defaultOptions();
   if (parameter) {
     const parsed = parseParameter(parameter);
@@ -113,6 +113,7 @@ export function optionsFromParameter(parameter: string): Options {
   if (!options.outputJsonMethods && !options.outputEncodeMethods && !options.outputClientImpl && !options.nestJs) {
     options.onlyTypes = true;
   }
+
   // Treat forceLong=true as LONG
   if ((options.forceLong as any) === true) {
     options.forceLong = LongOption.LONG;
@@ -130,6 +131,13 @@ export function optionsFromParameter(parameter: string): Options {
     // Treat useDate=false as TIMESTAMP
     options.useDate = DateOption.TIMESTAMP;
   }
+
+  if ((options.snakeToCamel as any) === false) {
+    options.snakeToCamel = [];
+  } else if ((options.snakeToCamel as any) === true) {
+    options.snakeToCamel = ['keys', 'json'];
+  }
+
   return options;
 }
 
diff --git a/src/utils.ts b/src/utils.ts
index be3f7530..ed868a5f 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -9,7 +9,7 @@ import {
 import ReadStream = NodeJS.ReadStream;
 import { SourceDescription } from './sourceInfo';
 import { Options, ServiceOption } from './options';
-import { camelCase, stringToSnakeCase } from './case';
+import { camelCase } from './case';
 
 export function protoFilesToGenerate(request: CodeGeneratorRequest): FileDescriptorProto[] {
   return request.protoFile.filter((f) => request.fileToGenerate.includes(f.name));
@@ -175,14 +175,13 @@ export class FormattedMethodDescriptor implements MethodDescriptorProto {
 }
 
 export function determineFieldJsonName(field: FieldDescriptorProto, options: Options): string {
-  // By default jsonName is camelCased by the protocol compilier unless the user has
-  // set a "json_name" option on this field.
-  const fromStringSnakeToCamel = typeof options.snakeToCamel === 'string' && options.snakeToCamel.includes('json');
-  const fromBooleanSnakeToCamel = options.snakeToCamel === true;
-  if (fromBooleanSnakeToCamel || fromStringSnakeToCamel) {
+  // jsonName will be camelCased by the protocol compiler, plus can be overridden by the user,
+  // so just use that instead of our own maybeSnakeToCamel
+  if (options.snakeToCamel.includes('json')) {
     return field.jsonName;
+  } else {
+    return field.name;
   }
-  return stringToSnakeCase(field.name);
 }
 
 export function impProto(options: Options, module: string, type: string): Import {


Test patch:
diff --git a/tests/case-test.ts b/tests/case-test.ts
index 33971df4..15c2536c 100644
--- a/tests/case-test.ts
+++ b/tests/case-test.ts
@@ -1,24 +1,27 @@
 import { maybeSnakeToCamel } from '../src/case';
+import { Options, optionsFromParameter } from '../src/options';
+
+const keys = optionsFromParameter('snakeToCamel=keys');
 
 describe('case', () => {
   it('converts snake to camel by default', () => {
-    expect(maybeSnakeToCamel('foo_bar', { snakeToCamel: true })).toEqual('fooBar');
+    expect(maybeSnakeToCamel('foo_bar', optionsFromParameter(undefined))).toEqual('fooBar');
   });
 
   it('leaves as-is if snakeToCamel is false', () => {
-    expect(maybeSnakeToCamel('foo_bar', { snakeToCamel: false })).toEqual('foo_bar');
+    expect(maybeSnakeToCamel('foo_bar', optionsFromParameter('snakeToCamel=false'))).toEqual('foo_bar');
   });
 
   it('de-upper cases', () => {
-    expect(maybeSnakeToCamel('FOO_BAR', { snakeToCamel: true })).toEqual('FooBar');
+    expect(maybeSnakeToCamel('FOO_BAR', keys)).toEqual('FooBar');
   });
 
   it('leaves the first character as it was', () => {
-    expect(maybeSnakeToCamel('Foo_Bar', { snakeToCamel: true })).toEqual('FooBar');
+    expect(maybeSnakeToCamel('Foo_Bar', keys)).toEqual('FooBar');
   });
 
   it('does nothing is already camel', () => {
-    expect(maybeSnakeToCamel('FooBar', { snakeToCamel: true })).toEqual('FooBar');
+    expect(maybeSnakeToCamel('FooBar', keys)).toEqual('FooBar');
   });
 
   // deal with original protoc which converts
@@ -26,14 +29,14 @@ describe('case', () => {
   // __uuid -> Uuid
   // _uuid_foo -> UuidFoo
   it('converts snake to camel with first underscore', () => {
-    expect(maybeSnakeToCamel('_uuid', { snakeToCamel: true })).toEqual('Uuid');
+    expect(maybeSnakeToCamel('_uuid', { snakeToCamel: ['keys'] })).toEqual('Uuid');
   });
 
   it('converts snake to camel with first double underscore', () => {
-    expect(maybeSnakeToCamel('__uuid', { snakeToCamel: true })).toEqual('Uuid');
+    expect(maybeSnakeToCamel('__uuid', { snakeToCamel: ['keys'] })).toEqual('Uuid');
   });
 
   it('converts snake to camel with first underscore and camelize other', () => {
-    expect(maybeSnakeToCamel('_uuid_foo', { snakeToCamel: true })).toEqual('UuidFoo');
+    expect(maybeSnakeToCamel('_uuid_foo', { snakeToCamel: ['keys'] })).toEqual('UuidFoo');
   });
 });
diff --git a/tests/options-test.ts b/tests/options-test.ts
index 357cf880..2a0c3612 100644
--- a/tests/options-test.ts
+++ b/tests/options-test.ts
@@ -26,7 +26,10 @@ describe('options', () => {
         "outputServices": "default",
         "outputTypeRegistry": false,
         "returnObservable": false,
-        "snakeToCamel": true,
+        "snakeToCamel": Array [
+          "json",
+          "keys",
+        ],
         "stringEnums": false,
         "unrecognizedEnum": true,
         "useDate": "timestamp",