Skip to content

Commit

Permalink
Merge pull request #1 from Yo-mah-Ya/dev
Browse files Browse the repository at this point in the history
updated optional & oneof handling
  • Loading branch information
Yo-mah-Ya committed Oct 26, 2023
2 parents 8c11d28 + 4652006 commit 9d96575
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 67 deletions.
131 changes: 85 additions & 46 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,88 @@ import {
FileDescriptorProto,
} from "./protobuf/descriptor";
import { getFieldName, getMessageOrEnumWithPackages } from "./field";
import { toTypeName } from "./types";
import { toOptional, toTypeName } from "./types";
import { EOL } from "os";
import prettier from "prettier";
import type { Option } from "./option";
import { comments } from "./comments";
import path from "path";
import { removeFileExtension } from "./util";

const createField = (
const getFieldSet = (
{
fileDescriptor,
fileDescriptorProto,
fieldDescriptor,
}: {
fileDescriptor: FileDescriptorProto;
fileDescriptorProto: FileDescriptorProto;
fieldDescriptor: FieldDescriptorProto;
},
options: Option,
): string => {
): {
comment: string;
fieldName: string;
optionalSign: string;
fieldType: string;
} => {
const comment = comments(fieldDescriptor.options?.deprecated);
const fieldType = toTypeName(fieldDescriptor, fileDescriptor);
const fieldType = toTypeName({
fieldDescriptor,
fileDescriptorProto,
});
const fieldName = getFieldName(fieldDescriptor, options);
return `${comment}readonly ${fieldName}?: ${fieldType};`;
const optionalSign = toOptional({ fieldDescriptor });
return {
comment,
fieldName,
optionalSign,
fieldType,
};
};

const createOneOfField = (
{
fileDescriptor,
fileDescriptorProto,
descriptorProto,
oneOfIndex,
}: {
fileDescriptor: FileDescriptorProto;
fileDescriptorProto: FileDescriptorProto;
descriptorProto: DescriptorProto;
oneOfIndex: number;
},
typeGuardFuncsForOneOf: string[],
options: Option,
): string => {
const fields = descriptorProto.field.filter(
(field) => field.oneof_index === oneOfIndex,
);
const oneOfName = descriptorProto.oneof_decl[oneOfIndex].name;
const unionType = fields
.map((f) => {
const fieldName = getFieldName(f, options);
const field = createField(
{
fileDescriptor,
fieldDescriptor: f,
},
const { comment, fieldName, fieldType } = getFieldSet(
{ fileDescriptorProto, fieldDescriptor: f },
options,
);
return `{ readonly $oneOfField: '${fieldName}', ${field} }`;
const variableName = `is${
fieldName.charAt(0).toUpperCase() + fieldName.slice(1)
}Of${oneOfName}Of${descriptorProto.name}`;
typeGuardFuncsForOneOf.push(`
export const ${variableName} = (v: ${descriptorProto.name}["${oneOfName}"]): v is { $oneOfField: "${fieldName}"; ${fieldName}: ${fieldType} } =>
v.$oneOfField === "${fieldName}";
`);
return `{ ${comment}readonly $oneOfField: '${fieldName}', ${fieldName}: ${fieldType} }`;
})
.join(" | ");
const oneOfName = descriptorProto.oneof_decl[oneOfIndex].name;
return `readonly ${oneOfName}: ${unionType}`;
};

export const generateMessage = (
{
fileDescriptor,
fileDescriptorProto,
descriptorProto,
}: { fileDescriptor: FileDescriptorProto; descriptorProto: DescriptorProto },
}: {
fileDescriptorProto: FileDescriptorProto;
descriptorProto: DescriptorProto;
},
options: Option,
): string => {
const chunks: string[] = [];
Expand All @@ -76,7 +97,7 @@ export const generateMessage = (
chunks.push(
generateMessage(
{
fileDescriptor,
fileDescriptorProto,
descriptorProto: nestedDescriptorProto,
},
options,
Expand All @@ -86,21 +107,36 @@ export const generateMessage = (

const processedOneOfIndex = new Set<number>();

const literalNode = descriptorProto.field.map((fieldDescriptor) => {
const literalNode: string[] = [];
const typeGuardFuncsForOneOf: string[] = [];

for (const fieldDescriptor of descriptorProto.field) {
const oneOfIndex = fieldDescriptor.oneof_index;
if (
descriptorProto.oneof_decl.length &&
!fieldDescriptor.proto3_optional &&
!processedOneOfIndex.has(oneOfIndex)
!processedOneOfIndex.has(oneOfIndex) &&
options.useTypeGuardForOneOf
) {
processedOneOfIndex.add(oneOfIndex);
return createOneOfField(
{ fileDescriptor, descriptorProto, oneOfIndex },
options,
literalNode.push(
createOneOfField(
{ fileDescriptorProto, descriptorProto, oneOfIndex },
typeGuardFuncsForOneOf,
options,
),
);
}
return createField({ fileDescriptor, fieldDescriptor }, options);
});
if (processedOneOfIndex.has(oneOfIndex)) continue;

const { comment, fieldName, optionalSign, fieldType } = getFieldSet(
{ fileDescriptorProto, fieldDescriptor },
options,
);
literalNode.push(
`${comment}readonly ${fieldName}${optionalSign}: ${fieldType};`,
);
}

const comment = comments(descriptorProto.options?.deprecated);
if (literalNode.length) {
Expand All @@ -112,6 +148,8 @@ export const generateMessage = (
} else {
chunks.push(`${comment}export interface ${descriptorProto.name}{${EOL}};`);
}

chunks.push(...typeGuardFuncsForOneOf);
return chunks.join(EOL);
};

Expand All @@ -134,46 +172,47 @@ export const generateEnum = (
};

export const generateFile = (
fileDescriptor: FileDescriptorProto,
fileDescriptorProto: FileDescriptorProto,
options: Option,
): Promise<string> => {
const statements: string[] = ["/* eslint-disable */"];

const enums = fileDescriptor.enum_type.flatMap((enumDescriptor) =>
const enums = fileDescriptorProto.enum_type.flatMap((enumDescriptor) =>
generateEnum(enumDescriptor, options),
);
const messages = fileDescriptor.message_type.flatMap((messageDescriptor) =>
generateMessage(
{
fileDescriptor,
descriptorProto: messageDescriptor,
},
options,
),
const messages = fileDescriptorProto.message_type.flatMap(
(messageDescriptor) =>
generateMessage(
{
fileDescriptorProto,
descriptorProto: messageDescriptor,
},
options,
),
);

statements.push(
fileDescriptor.dependency
fileDescriptorProto.dependency
.map((dependency) => {
if (!fileDescriptor.name) return;
if (!fileDescriptor?.package) return;
if (!fileDescriptorProto.name) return;
if (!fileDescriptorProto?.package) return;

const types = getMessageOrEnumWithPackages(fileDescriptor.package);
const types = getMessageOrEnumWithPackages(fileDescriptorProto.package);
if (!types?.length) return;
return `import {${types.join(", ")}} from "${path.relative(
path.dirname(fileDescriptor.name),
path.dirname(fileDescriptorProto.name),
removeFileExtension(dependency, ""),
)}";`;
})
.join(EOL),
);

if (fileDescriptor.syntax) {
statements.push(`export const syntax = "${fileDescriptor.syntax}";`);
if (fileDescriptorProto.syntax) {
statements.push(`export const syntax = "${fileDescriptorProto.syntax}";`);
}
if (fileDescriptor.package) {
if (fileDescriptorProto.package) {
statements.push(
`export const protocolBufferPackage = "${fileDescriptor.package}";`,
`export const protocolBufferPackage = "${fileDescriptorProto.package}";`,
);
}

Expand Down
46 changes: 29 additions & 17 deletions src/option.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import { toOptions } from "./option";

describe(toOptions, () => {
const defaultOption = {
useJsonName: false,
enumValueAsString: false,
useTypeGuardForOneOf: false,
};
test("no option", () => {
expect(toOptions("")).toStrictEqual({
useJsonName: false,
enumValueAsString: false,
});
expect(toOptions("")).toStrictEqual(defaultOption);
});
describe("unparsable", () => {
test("delimited by other than comma", () => {
const warnLog = jest.spyOn(console, "warn");
expect(
toOptions("useJsonName=true:enumValueAsString=true"),
).toStrictEqual({
useJsonName: false,
enumValueAsString: false,
});
).toStrictEqual(defaultOption);
expect(warnLog).toHaveBeenCalled();
});

test("not delimited by equal sign", () => {
const warnLog = jest.spyOn(console, "warn");
expect(toOptions("useJsonName")).toStrictEqual({
useJsonName: false,
enumValueAsString: false,
});
expect(toOptions("useJsonName")).toStrictEqual(defaultOption);
expect(warnLog).toHaveBeenCalled();
});
});

test("unknown parameter", () => {
expect(toOptions("key=value")).toStrictEqual(defaultOption);
});
});

describe("individual options", () => {
test("useJsonName", () => {
expect(toOptions("useJsonName=false")).toHaveProperty("useJsonName", false);
expect(toOptions("useJsonName=true")).toHaveProperty("useJsonName", true);
Expand All @@ -54,10 +56,20 @@ describe(toOptions, () => {
expect(warnLog).toHaveBeenCalled();
});

test("unknown parameter", () => {
expect(toOptions("key=value")).toStrictEqual({
useJsonName: false,
enumValueAsString: false,
});
test("useTypeGuardForOneOf", () => {
expect(toOptions("useTypeGuardForOneOf=false")).toHaveProperty(
"useTypeGuardForOneOf",
false,
);
expect(toOptions("useTypeGuardForOneOf=true")).toHaveProperty(
"useTypeGuardForOneOf",
true,
);
const warnLog = jest.spyOn(console, "warn");
expect(toOptions("useTypeGuardForOneOf=dummy")).toHaveProperty(
"useTypeGuardForOneOf",
false,
);
expect(warnLog).toHaveBeenCalled();
});
});
11 changes: 11 additions & 0 deletions src/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { EOL } from "os";
export type Option = {
useJsonName: boolean;
enumValueAsString: boolean;
useTypeGuardForOneOf: boolean;
};

export const toOptions = (parameters: string): Option => {
const defaultOption: Option = {
useJsonName: false,
enumValueAsString: false,
useTypeGuardForOneOf: false,
};
if (parameters === "") {
return defaultOption;
Expand Down Expand Up @@ -45,6 +47,15 @@ export const toOptions = (parameters: string): Option => {
}
continue;
}
case "useTypeGuardForOneOf":
if (["true", "false"].includes(value)) {
defaultOption.useTypeGuardForOneOf = "true" === value;
} else {
console.warn(
'"useTypeGuardForOneOf" option must be either "true" or "false"',
);
}
continue;
default:
continue;
}
Expand Down
36 changes: 32 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DescriptorProto,
FieldDescriptorProto,
FieldDescriptorProto_Label,
FieldDescriptorProto_Type,
Expand All @@ -15,10 +16,22 @@ export const isRequired = (fieldDescriptor: FieldDescriptorProto): boolean =>
export const isRepeated = (fieldDescriptor: FieldDescriptorProto): boolean =>
fieldDescriptor.label === FieldDescriptorProto_Label.LABEL_REPEATED;

export const toTypeName = (
fieldDescriptor: FieldDescriptorProto,
fileDescriptorProto: FileDescriptorProto,
): string => {
export const hasOneOf = (descriptorProto: DescriptorProto): boolean =>
descriptorProto.oneof_decl.length > 0;

export const isMessage = (
fieldDescriptorProto: FieldDescriptorProto,
): boolean =>
fieldDescriptorProto.type === FieldDescriptorProto_Type.TYPE_MESSAGE ||
fieldDescriptorProto.type === FieldDescriptorProto_Type.TYPE_GROUP;

export const toTypeName = ({
fieldDescriptor,
fileDescriptorProto,
}: {
fieldDescriptor: FieldDescriptorProto;
fileDescriptorProto: FileDescriptorProto;
}): string => {
switch (fieldDescriptor.type) {
case FieldDescriptorProto_Type.TYPE_DOUBLE:
case FieldDescriptorProto_Type.TYPE_FLOAT:
Expand Down Expand Up @@ -58,3 +71,18 @@ export const toTypeName = (
}
}
};

export const toOptional = ({
fieldDescriptor,
}: {
fieldDescriptor: FieldDescriptorProto;
}): "?" | "" => {
if (
fieldDescriptor.proto3_optional ||
(isMessage(fieldDescriptor) && !isRepeated(fieldDescriptor))
) {
return "?";
}

return "";
};

0 comments on commit 9d96575

Please sign in to comment.