Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for 'json_name' annotation. #210

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ The test suite's proto files (i.e. `simple.proto`, `batching.proto`, etc.) curre
# Todo

- Support the string-based encoding of duration in `fromJSON`/`toJSON`
- Support the `json_name` annotation
- Make `oneof=unions` the default behavior in 2.0
- Probably change `forceLong` default in 2.0, should default to `forceLong=long`
- Make `esModuleInterop=true` the default in 2.0
Expand Down
8 changes: 8 additions & 0 deletions integration/simple-json-name/simple-json-name-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Simple } from './simple';

describe('simple', () => {
it('generates field names correctly', () => {
const simple: Simple = Simple.fromPartial({});
expect(Object.prototype.hasOwnProperty.call(simple, 'other_name')).toBe(true);
});
});
Binary file added integration/simple-json-name/simple.bin
Binary file not shown.
7 changes: 7 additions & 0 deletions integration/simple-json-name/simple.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto3";

package simple;

message Simple {
string name = 1 [ json_name = "other_name" ];
}
72 changes: 72 additions & 0 deletions integration/simple-json-name/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable */
import { Writer, Reader } from 'protobufjs/minimal';

export const protobufPackage = 'simple';

export interface Simple {
other_name: string;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, my interpretation of json_name attribute would be that this particular name would still be name and then other_name would only show up on the JSON on the wire (i.e. in toJson methods) and then looked for when data is parsed in from the wire (i.e. in the fromJson methods).

Can we do that instead?

}

const baseSimple: object = { other_name: '' };

export const Simple = {
encode(message: Simple, writer: Writer = Writer.create()): Writer {
writer.uint32(10).string(message.other_name);
return writer;
},

decode(input: Reader | Uint8Array, length?: number): Simple {
const reader = input instanceof Uint8Array ? new Reader(input) : input;
let end = length === undefined ? reader.len : reader.pos + length;
const message = { ...baseSimple } as Simple;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.other_name = reader.string();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},

fromJSON(object: any): Simple {
const message = { ...baseSimple } as Simple;
if (object.other_name !== undefined && object.other_name !== null) {
message.other_name = String(object.other_name);
} else {
message.other_name = '';
}
return message;
},

fromPartial(object: DeepPartial<Simple>): Simple {
const message = { ...baseSimple } as Simple;
if (object.other_name !== undefined && object.other_name !== null) {
message.other_name = object.other_name;
} else {
message.other_name = '';
}
return message;
},

toJSON(message: Simple): unknown {
const obj: any = {};
message.other_name !== undefined && (obj.other_name = message.other_name);
return obj;
},
};

type Builtin = Date | Function | Uint8Array | string | number | undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
31 changes: 19 additions & 12 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ import FieldDescriptorProto = google.protobuf.FieldDescriptorProto;
import FileDescriptorProto = google.protobuf.FileDescriptorProto;
import ServiceDescriptorProto = google.protobuf.ServiceDescriptorProto;

function getFieldName(field: FieldDescriptorProto, options: Options): string {
if (field.jsonName.length > 0) {
return field.jsonName;
}
return maybeSnakeToCamel(field.name, options);
}

export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [string, Code] {
const { options, utils: u } = ctx;

Expand Down Expand Up @@ -441,7 +448,7 @@ function generateInterfaceDeclaration(
const info = sourceInfo.lookup(Fields.message.field, index);
maybeAddComment(info, chunks, fieldDesc.options?.deprecated);

const name = maybeSnakeToCamel(fieldDesc.name, options);
const name = getFieldName(fieldDesc, options);
const type = toTypeName(ctx, messageDesc, fieldDesc);
const q = isOptionalProperty(fieldDesc, options) ? '?' : '';
chunks.push(code`${name}${q}: ${type}, `);
Expand All @@ -461,7 +468,7 @@ function generateOneofProperty(
const fields = messageDesc.field.filter((field) => isWithinOneOf(field) && field.oneofIndex === oneofIndex);
const unionType = joinCode(
fields.map((f) => {
let fieldName = maybeSnakeToCamel(f.name, options);
let fieldName = getFieldName(f, options);
let typeName = toTypeName(ctx, messageDesc, f);
return code`{ $case: '${fieldName}', ${fieldName}: ${typeName} }`;
}),
Expand All @@ -483,7 +490,7 @@ function generateOneofProperty(
return;
}
const info = sourceInfo.lookup(Fields.message.field, index);
const name = maybeSnakeToCamel(field.name, options);
const name = getFieldName(field, options);
maybeAddComment(info, (text) => comments.push(name + '\n' + text));
});
if (comments.length) {
Expand All @@ -500,7 +507,7 @@ function generateBaseInstance(ctx: Context, fullName: string, messageDesc: Descr
.map((field) => [field, defaultValue(ctx, field)])
.filter(([field, val]) => val !== 'undefined' && !isBytes(field))
.map(([field, val]) => {
const name = maybeSnakeToCamel(field.name, ctx.options);
const name = getFieldName(field, ctx.options);
return code`${name}: ${val}`;
});
return code`const base${fullName}: object = { ${joinCode(fields, { on: ',' })} };`;
Expand Down Expand Up @@ -535,7 +542,7 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP

// initialize all lists
messageDesc.field.filter(isRepeated).forEach((field) => {
const name = maybeSnakeToCamel(field.name, options);
const name = getFieldName(field, options);
const value = isMapType(ctx, messageDesc, field) ? '{}' : '[]';
chunks.push(code`message.${name} = ${value};`);
});
Expand All @@ -549,7 +556,7 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP

// add a case for each incoming field
messageDesc.field.forEach((field) => {
const fieldName = maybeSnakeToCamel(field.name, options);
const fieldName = getFieldName(field, options);
chunks.push(code`case ${field.number}:`);

// get a generic 'reader.doSomething' bit that is specific to the basic type
Expand Down Expand Up @@ -651,7 +658,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP

// then add a case for each field
messageDesc.field.forEach((field) => {
const fieldName = maybeSnakeToCamel(field.name, options);
const fieldName = getFieldName(field, options);

// get a generic writer.doSomething based on the basic type
let writeSnippet: (place: string) => Code;
Expand Down Expand Up @@ -747,13 +754,13 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto
// initialize all lists
messageDesc.field.filter(isRepeated).forEach((field) => {
const value = isMapType(ctx, messageDesc, field) ? '{}' : '[]';
const name = maybeSnakeToCamel(field.name, options);
const name = getFieldName(field, options);
chunks.push(code`message.${name} = ${value};`);
});

// add a check for each incoming field
messageDesc.field.forEach((field) => {
const fieldName = maybeSnakeToCamel(field.name, options);
const fieldName = getFieldName(field, options);

// get a generic 'reader.doSomething' bit that is specific to the basic type
const readSnippet = (from: string): Code => {
Expand Down Expand Up @@ -874,7 +881,7 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP

// then add a case for each field
messageDesc.field.forEach((field) => {
const fieldName = maybeSnakeToCamel(field.name, options);
const fieldName = getFieldName(field, options);

const readSnippet = (from: string): Code => {
if (isEnum(field)) {
Expand Down Expand Up @@ -965,13 +972,13 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri
// initialize all lists
messageDesc.field.filter(isRepeated).forEach((field) => {
const value = isMapType(ctx, messageDesc, field) ? '{}' : '[]';
const name = maybeSnakeToCamel(field.name, options);
const name = getFieldName(field, options);
chunks.push(code`message.${name} = ${value};`);
});

// add a check for each incoming field
messageDesc.field.forEach((field) => {
const fieldName = maybeSnakeToCamel(field.name, options);
const fieldName = getFieldName(field, options);

const readSnippet = (from: string): Code => {
if (isEnum(field) || isPrimitive(field) || isTimestamp(field) || isValueType(ctx, field)) {
Expand Down