neoproto 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +23 -0
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/data/enum.proto +36 -0
- package/data/example.proto +42 -0
- package/package.json +36 -0
- package/patches/protobufjs-cli+2.0.0.patch +10 -0
- package/src/generators/serialization.ts +129 -0
- package/src/generators/test.ts +206 -0
- package/src/generators/traits.ts +141 -0
- package/src/generators/types.ts +191 -0
- package/src/generators/unwrap.ts +333 -0
- package/src/generators/wrap.ts +334 -0
- package/src/index.ts +249 -0
- package/src/pbcli.ts +49 -0
- package/src/utils/array-manipulation.ts +19 -0
- package/src/utils/associations.ts +53 -0
- package/src/utils/comments.ts +17 -0
- package/src/utils/logger.ts +22 -0
- package/src/utils/protobuf.ts +73 -0
- package/src/utils/string-manipulation.ts +26 -0
- package/src/utils/system.ts +54 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
npm-publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
registry-url: https://registry.npmjs.org/
|
|
19
|
+
- run: npm ci
|
|
20
|
+
- run: npm test
|
|
21
|
+
- run: npm publish
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kollin Murphy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# neoproto
|
|
2
|
+
|
|
3
|
+
`neoproto` is a code generation tool that focuses on improving the developer experience of working with protobuf messages in TypeScript. It generates usable type definitions, serialization and deserialization functions, and test cases for protobuf messages defined in `.proto` files. The goal is to produce an idiomatic TypeScript API that abstracts away the idiosyncrasies of protobuf as much as possible.
|
|
4
|
+
|
|
5
|
+
### Limitations
|
|
6
|
+
|
|
7
|
+
This is a strongly opinionated library that makes certain assumptions about how protobuf messages are defined and used. It is not a general-purpose library. You may find it helpful, or you may find that it does not fit your use case. In either case, please feel free to fork the library and modify it to suit your needs.
|
|
8
|
+
|
|
9
|
+
This project supports only a subset of the `proto2` syntax (messages and enums). `proto3` is not supported.
|
|
10
|
+
|
|
11
|
+
As defined in the [protobuf spec version 2](https://protobuf.dev/programming-guides/proto2/#default), the default values of fields are as follows:
|
|
12
|
+
|
|
13
|
+
| Type | Default Value |
|
|
14
|
+
| ------------- | ------------- |
|
|
15
|
+
| string | empty string |
|
|
16
|
+
| bytes | empty bytes |
|
|
17
|
+
| bool | false |
|
|
18
|
+
| numeric types | zero |
|
|
19
|
+
| enum | first value |
|
|
20
|
+
|
|
21
|
+
Given the above, there is no way to differentiate between a field that is set to its default value and a field that is not set at all. This can lead to ambiguity in certain cases, especially when dealing with optional fields. This library has taken the approach of treating all optional fields as if they were set to the default value. Ensure that your protobuf messages are designed with this in mind to avoid unintended consequences, and that usage of the library aligns with this behavior.
|
package/data/enum.proto
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
syntax = "proto2";
|
|
2
|
+
|
|
3
|
+
package My_Enum_ApiV2;
|
|
4
|
+
|
|
5
|
+
/** This is an example of an enum definition in Protocol Buffers. */
|
|
6
|
+
enum Corpus {
|
|
7
|
+
CORPUS_UNSPECIFIED = 0;
|
|
8
|
+
CORPUS_UNIVERSAL = 1;
|
|
9
|
+
CORPUS_WEB = 2;
|
|
10
|
+
CORPUS_IMAGES = 3;
|
|
11
|
+
CORPUS_LOCAL = 4;
|
|
12
|
+
CORPUS_NEWS = 5;
|
|
13
|
+
CORPUS_PRODUCTS = 6;
|
|
14
|
+
CORPUS_VIDEO = 7;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* MessageId: 3 */
|
|
18
|
+
message SearchRequest {
|
|
19
|
+
|
|
20
|
+
message Nested {
|
|
21
|
+
required int32 id = 1;
|
|
22
|
+
required string name = 2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
optional string query = 1;
|
|
26
|
+
optional int32 page_number = 2;
|
|
27
|
+
optional int32 results_per_page = 3;
|
|
28
|
+
required Corpus corpus1 = 4;
|
|
29
|
+
required Corpus corpus2 = 5;
|
|
30
|
+
required Nested nested = 6;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* MessageId: 4 */
|
|
34
|
+
message SearchResponse {
|
|
35
|
+
required string results = 1;
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Text 1
|
|
2
|
+
syntax = "proto2";
|
|
3
|
+
|
|
4
|
+
// Text 2
|
|
5
|
+
|
|
6
|
+
/* Test 439104 */
|
|
7
|
+
package My_Api_V1;
|
|
8
|
+
|
|
9
|
+
message MyNestedMessage {
|
|
10
|
+
required int32 id = 1;
|
|
11
|
+
required int64 some_other_long = 2;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* A simple message for demonstration purposes.
|
|
15
|
+
This is a continuation.
|
|
16
|
+
* More stuff
|
|
17
|
+
* MessageId: 1
|
|
18
|
+
*/
|
|
19
|
+
message MyMessageReq {
|
|
20
|
+
/* Text 3 */
|
|
21
|
+
required string value = 1;
|
|
22
|
+
optional int64 some_long = 2;
|
|
23
|
+
optional MyNestedMessage nested = 3;
|
|
24
|
+
required bool some_thing = 4;
|
|
25
|
+
repeated string some_repeated_string = 5;
|
|
26
|
+
repeated MyNestedMessage some_repeated_nested = 6;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* This is a response message for MyMessageReq.
|
|
31
|
+
* It contains the same fields as MyMessageReq, but it is a separate message type.
|
|
32
|
+
* MessageId: 2
|
|
33
|
+
*/
|
|
34
|
+
message MyMessageRes {
|
|
35
|
+
required string value = 1;
|
|
36
|
+
optional int64 some_long = 2;
|
|
37
|
+
optional MyNestedMessage nested = 3;
|
|
38
|
+
required bool some_thing = 4;
|
|
39
|
+
repeated string some_repeated_string = 5;
|
|
40
|
+
repeated MyNestedMessage some_repeated_nested = 6;
|
|
41
|
+
required bytes binary_data = 7;
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "neoproto",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/kollinmurphy/neoproto.git"
|
|
7
|
+
},
|
|
8
|
+
"description": "An opinionated wrapper around the Protobuf TypeScript compiler that generates idiomatic types, serialization functions, and documentation.",
|
|
9
|
+
"main": "src/index.ts",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "tsx src/index.ts",
|
|
13
|
+
"postinstall": "patch-package"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"protobuf",
|
|
17
|
+
"typescript",
|
|
18
|
+
"codegen",
|
|
19
|
+
"serialization",
|
|
20
|
+
"deserialization"
|
|
21
|
+
],
|
|
22
|
+
"author": "Kollin Murphy",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.2.3",
|
|
26
|
+
"patch-package": "^8.0.1",
|
|
27
|
+
"tsx": "^4.21.0",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"long": "^5.3.2",
|
|
32
|
+
"prettier": "^3.8.1",
|
|
33
|
+
"protobufjs": "^7.5.4",
|
|
34
|
+
"protobufjs-cli": "^2.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
diff --git a/node_modules/protobufjs-cli/wrappers/es6.js b/node_modules/protobufjs-cli/wrappers/es6.js
|
|
2
|
+
index 5bdc43c..33246d7 100644
|
|
3
|
+
--- a/node_modules/protobufjs-cli/wrappers/es6.js
|
|
4
|
+
+++ b/node_modules/protobufjs-cli/wrappers/es6.js
|
|
5
|
+
@@ -1,4 +1,4 @@
|
|
6
|
+
-import * as $protobuf from $DEPENDENCY;
|
|
7
|
+
+import $protobuf from $DEPENDENCY;
|
|
8
|
+
|
|
9
|
+
$OUTPUT;
|
|
10
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import proto from "protobufjs";
|
|
2
|
+
import { capitalizeFirstLetter } from "../utils/string-manipulation.js";
|
|
3
|
+
import { getWrapperFunctionName } from "./wrap.js";
|
|
4
|
+
import { getUnwrapperFunctionName } from "./unwrap.js";
|
|
5
|
+
import { getMessages } from "../utils/protobuf.js";
|
|
6
|
+
import { getMessageId } from "../utils/associations.js";
|
|
7
|
+
import { logError } from "../utils/logger.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates wrapper functions for all messages and enums in a protobuf namespace, including nested namespaces, and returns the content of the wrapper file as a string.
|
|
11
|
+
* @param namespace - The protobuf namespace to create wrapper functions for
|
|
12
|
+
* @returns A string containing the content of the wrapper file with all the generated wrapper functions for the protobuf namespace
|
|
13
|
+
*/
|
|
14
|
+
function createNamespaceSerializers(namespace: proto.Namespace): string {
|
|
15
|
+
let definitions = "";
|
|
16
|
+
const exports: string[] = [];
|
|
17
|
+
const typeImports: string[] = [];
|
|
18
|
+
const wrapperImports: string[] = [];
|
|
19
|
+
const unwrapperImports: string[] = [];
|
|
20
|
+
const messages = getMessages(namespace);
|
|
21
|
+
for (const message of messages) {
|
|
22
|
+
if (!getMessageId(message)) continue;
|
|
23
|
+
const {
|
|
24
|
+
definitions: messageDefinitions,
|
|
25
|
+
exports: messageExports,
|
|
26
|
+
typeImports: messageImports,
|
|
27
|
+
wrapperImports: messageWrapperImports,
|
|
28
|
+
unwrapperImports: messageUnwrapperImports,
|
|
29
|
+
} = createMessageSerializers(namespace.name, message);
|
|
30
|
+
definitions += messageDefinitions;
|
|
31
|
+
exports.push(...messageExports);
|
|
32
|
+
typeImports.push(...messageImports);
|
|
33
|
+
wrapperImports.push(...messageWrapperImports);
|
|
34
|
+
unwrapperImports.push(...messageUnwrapperImports);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!definitions)
|
|
38
|
+
logError(
|
|
39
|
+
`No messages with a valid message ID found in namespace ${namespace.name}.`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const dedupedTypeImports = [...new Set(typeImports)];
|
|
43
|
+
const dedupedWrapperImports = [...new Set(wrapperImports)];
|
|
44
|
+
const dedupedUnwrapperImports = [...new Set(unwrapperImports)];
|
|
45
|
+
return `import type { ${dedupedTypeImports.join(", ")} } from "./types.js";
|
|
46
|
+
import { ${namespace.name} } from "./protobuf/${namespace.name}.js";
|
|
47
|
+
import { ${dedupedWrapperImports.join(", ")} } from "./wrap.js";
|
|
48
|
+
import { ${dedupedUnwrapperImports.join(", ")} } from "./unwrap.js";
|
|
49
|
+
${definitions}
|
|
50
|
+
export {
|
|
51
|
+
${exports.join(",\n ")},
|
|
52
|
+
};
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generates a serializer and deserializer function for a single protobuf message type. The serializer function takes an instance of the message type and returns a Uint8Array containing the serialized message, while the deserializer function takes a Uint8Array containing the serialized message and returns an instance of the message type. The functions use the protobufjs library to perform the encoding and decoding, and they also utilize wrapper and unwrapper functions to convert between the protobuf message format and the TypeScript types defined for the message.
|
|
58
|
+
* @param namespace - The name of the protobuf namespace that the message type belongs to, which is used to reference the correct message type in the protobufjs encoding and decoding functions
|
|
59
|
+
* @param message - The protobuf message type to create the serializer and deserializer functions for
|
|
60
|
+
* @returns An object containing the TypeScript definitions for the serializer and deserializer functions, the names of the functions to be exported, and arrays of imports required for the message types and wrapper/unwrapper functions used in the definitions
|
|
61
|
+
*/
|
|
62
|
+
function createMessageSerializers(
|
|
63
|
+
namespace: string,
|
|
64
|
+
message: proto.Type,
|
|
65
|
+
): {
|
|
66
|
+
definitions: string;
|
|
67
|
+
exports: string[];
|
|
68
|
+
typeImports: string[];
|
|
69
|
+
wrapperImports: string[];
|
|
70
|
+
unwrapperImports: string[];
|
|
71
|
+
} {
|
|
72
|
+
const functionSuffix = capitalizeFirstLetter(message.name);
|
|
73
|
+
|
|
74
|
+
const serialize = `serialize${functionSuffix}`;
|
|
75
|
+
const deserialize = `deserialize${functionSuffix}`;
|
|
76
|
+
const wrap = getWrapperFunctionName(message.name);
|
|
77
|
+
const unwrap = getUnwrapperFunctionName(message.name);
|
|
78
|
+
|
|
79
|
+
const definitions = `
|
|
80
|
+
/**
|
|
81
|
+
* Serializes a ${message.name} message to a Uint8Array.
|
|
82
|
+
* @param input ${message.name} message to serialize
|
|
83
|
+
* @returns Uint8Array containing the serialized message
|
|
84
|
+
*/
|
|
85
|
+
function ${serialize}(input: ${message.name}): Uint8Array {
|
|
86
|
+
return ${namespace}.${message.name}.encode(${unwrap}(input)).finish();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Deserializes a ${message.name} message from a Uint8Array.
|
|
91
|
+
* @param input Uint8Array containing the serialized message
|
|
92
|
+
* @returns ${message.name} message
|
|
93
|
+
*/
|
|
94
|
+
function ${deserialize}(input: Uint8Array): ${message.name} {
|
|
95
|
+
return ${wrap}(${namespace}.${message.name}.decode(input));
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
return {
|
|
99
|
+
definitions,
|
|
100
|
+
exports: [serialize, deserialize],
|
|
101
|
+
typeImports: [message.name],
|
|
102
|
+
wrapperImports: [wrap],
|
|
103
|
+
unwrapperImports: [unwrap],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generates a serializer and deserializer function name for a given protobuf message name by capitalizing the first letter of the message name and prefixing it with "serialize" for the serializer function and "deserialize" for the deserializer function. For example, if the message name is "MyMessage", the generated serializer function name would be "serializeMyMessage" and the deserializer function name would be "deserializeMyMessage".
|
|
109
|
+
* @param messageName - The name of the protobuf message type to generate the serializer and deserializer function names for
|
|
110
|
+
* @returns An object containing the generated serializer and deserializer function names for the specified protobuf message type
|
|
111
|
+
*/
|
|
112
|
+
function getSerializerFunctionName(messageName: string) {
|
|
113
|
+
return `serialize${capitalizeFirstLetter(messageName)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generates a deserializer function name for a given protobuf message name by capitalizing the first letter of the message name and prefixing it with "deserialize". For example, if the message name is "MyMessage", the generated deserializer function name would be "deserializeMyMessage".
|
|
118
|
+
* @param messageName - The name of the protobuf message type to generate the deserializer function name for
|
|
119
|
+
* @returns A string containing the generated deserializer function name for the specified protobuf message type
|
|
120
|
+
*/
|
|
121
|
+
function getDeserializerFunctionName(messageName: string) {
|
|
122
|
+
return `deserialize${capitalizeFirstLetter(messageName)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
createNamespaceSerializers,
|
|
127
|
+
getSerializerFunctionName,
|
|
128
|
+
getDeserializerFunctionName,
|
|
129
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import proto from "protobufjs";
|
|
2
|
+
import {
|
|
3
|
+
getMessages,
|
|
4
|
+
hasOptionalField,
|
|
5
|
+
isRequiredField,
|
|
6
|
+
} from "../utils/protobuf.js";
|
|
7
|
+
import { getMessageId } from "../utils/associations.js";
|
|
8
|
+
import {
|
|
9
|
+
getDeserializerFunctionName,
|
|
10
|
+
getSerializerFunctionName,
|
|
11
|
+
} from "./serialization.js";
|
|
12
|
+
import { logError } from "../utils/logger.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Defines the behavior for handling optional fields when creating test
|
|
16
|
+
* instances for protobuf messages. The "all-required" behavior includes all
|
|
17
|
+
* fields as required, the "omit-optional" behavior omits optional fields from
|
|
18
|
+
* the test instance, and the "default-optional" behavior includes optional
|
|
19
|
+
* fields with default values (e.g., empty string for strings, 0 for numbers,
|
|
20
|
+
* false for booleans). This type is used to specify how optional fields should
|
|
21
|
+
* be treated when generating test cases for protobuf messages in the generated
|
|
22
|
+
* test file.
|
|
23
|
+
*/
|
|
24
|
+
type OptionalBehavior = "all-required" | "omit-optional" | "default-optional";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates test cases for all messages in a protobuf namespace, including nested namespaces, and returns the content of the test file as a string. The generated tests include serialization and deserialization tests for each message, as well as additional tests for handling optional fields if any are present in the message definitions.
|
|
28
|
+
* @param namespace - The protobuf namespace to create test cases for
|
|
29
|
+
* @param relativePath - The relative path to the generated index file for the protobuf namespace, which is used for importing the serializer and deserializer functions in the generated test file
|
|
30
|
+
* @returns A string containing the content of the test file with all the generated test cases for the protobuf namespace
|
|
31
|
+
*/
|
|
32
|
+
function generateNamespaceTests(
|
|
33
|
+
namespace: proto.Namespace,
|
|
34
|
+
relativePath: string,
|
|
35
|
+
) {
|
|
36
|
+
const messages = getMessages(namespace).filter((msg) =>
|
|
37
|
+
Boolean(getMessageId(msg)),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return `
|
|
41
|
+
import { describe, it } from 'node:test';
|
|
42
|
+
import assert from 'node:assert';
|
|
43
|
+
import { ${messages
|
|
44
|
+
.flatMap((msg) => [
|
|
45
|
+
getSerializerFunctionName(msg.name),
|
|
46
|
+
getDeserializerFunctionName(msg.name),
|
|
47
|
+
`type ${msg.name}`,
|
|
48
|
+
])
|
|
49
|
+
.join(", ")} } from "${relativePath}/index.js";
|
|
50
|
+
|
|
51
|
+
describe("${namespace.name}", () => {
|
|
52
|
+
${messages.map((msg) => generateMessageTests(msg)).join("\n")}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates test cases for a single protobuf message type, including serialization and deserialization tests, as well as additional tests for handling optional fields if any are present in the message definition. The generated tests create instances of the message with different combinations of required and optional fields to ensure that the serializer and deserializer functions handle all cases correctly.
|
|
60
|
+
* @param message - The protobuf message type to create test cases for
|
|
61
|
+
* @returns A string containing the test cases for the specified protobuf message type, including serialization and deserialization tests and optional field handling tests if applicable
|
|
62
|
+
*/
|
|
63
|
+
function generateMessageTests(message: proto.Type): string {
|
|
64
|
+
const serializer = getSerializerFunctionName(message.name);
|
|
65
|
+
const deserializer = getDeserializerFunctionName(message.name);
|
|
66
|
+
const optional = hasOptionalField(message);
|
|
67
|
+
const optionalTest = optional
|
|
68
|
+
? `it("should handle optional fields correctly", () => {
|
|
69
|
+
const original: ${message.name} = ${createTestInstance(message, "omit-optional")};
|
|
70
|
+
const defaulted: ${message.name} = ${createTestInstance(message, "default-optional")};
|
|
71
|
+
const serialized = ${serializer}(original);
|
|
72
|
+
const deserialized = ${deserializer}(serialized);
|
|
73
|
+
assert.deepStrictEqual(deserialized, defaulted);
|
|
74
|
+
});`
|
|
75
|
+
: "";
|
|
76
|
+
return `
|
|
77
|
+
describe("${message.name}", () => {
|
|
78
|
+
it("should serialize and deserialize correctly", () => {
|
|
79
|
+
const original: ${message.name} = ${createTestInstance(message, "all-required")};
|
|
80
|
+
const serialized = ${serializer}(original);
|
|
81
|
+
const deserialized = ${deserializer}(serialized);
|
|
82
|
+
assert.deepStrictEqual(deserialized, original);
|
|
83
|
+
});
|
|
84
|
+
${optionalTest}
|
|
85
|
+
});
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates a test instance of a protobuf message type with example values for each field, handling optional fields according to the specified behavior. The function generates a string representation of a TypeScript object that can be used as a test instance for the message type in serialization and deserialization tests. For repeated fields, it generates an array of example values, and for optional fields, it either omits them or sets them to default values based on the provided optional behavior.
|
|
91
|
+
* @param message - The protobuf message type to create a test instance for
|
|
92
|
+
* @param optionalBehavior - The behavior to apply for optional fields when creating the test instance, which can be "all-required" to include all fields as required, "omit-optional" to omit optional fields from the test instance, or "default-optional" to include optional fields with default values (e.g., empty string for strings, 0 for numbers, false for booleans)
|
|
93
|
+
* @returns A string containing the TypeScript object representation of a test instance for the specified protobuf message type, with example values for each field and handling of optional fields according to the specified behavior
|
|
94
|
+
*/
|
|
95
|
+
function createTestInstance(
|
|
96
|
+
message: proto.Type,
|
|
97
|
+
optionalBehavior: OptionalBehavior,
|
|
98
|
+
): string {
|
|
99
|
+
return `{
|
|
100
|
+
${message.fieldsArray
|
|
101
|
+
.map((field) => createTestFieldValue(field, optionalBehavior))
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join("\n")}
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Creates a test value for a single field of a protobuf message type, handling optional fields according to the specified behavior. The function generates a string representation of the field assignment that can be included in a test instance for the message type. For repeated fields, it generates an array of example values, and for optional fields, it either omits them or sets them to default values based on the provided optional behavior.
|
|
109
|
+
* @param field - The protobuf field to create a test value for
|
|
110
|
+
* @param optionalBehavior - The behavior to apply for optional fields when creating the test value, which can be "all-required" to include all fields as required, "omit-optional" to omit optional fields from the test value, or "default-optional" to include optional fields with default values (e.g., empty string for strings, 0 for numbers, false for booleans)
|
|
111
|
+
* @param omitFieldName - A boolean flag indicating whether to omit the field name in the generated test value, which is used for generating values for repeated fields where the field name is not included in the array elements
|
|
112
|
+
* @returns A string containing the TypeScript representation of the test value for the specified protobuf field, with handling of optional fields according to the specified behavior. If the field is omitted due to being optional and the optional behavior is "omit-optional", it returns null.
|
|
113
|
+
*/
|
|
114
|
+
function createTestFieldValue(
|
|
115
|
+
field: proto.Field,
|
|
116
|
+
optionalBehavior: OptionalBehavior,
|
|
117
|
+
omitFieldName = false,
|
|
118
|
+
): string | null {
|
|
119
|
+
const isOptional = !isRequiredField(field);
|
|
120
|
+
if (optionalBehavior === "omit-optional" && isOptional && !field.repeated) {
|
|
121
|
+
return null; // Skip optional fields if optionalBehavior is "omit-optional"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const fieldName = field.name;
|
|
125
|
+
const fieldType = field.resolvedType ? field.resolvedType.name : field.type;
|
|
126
|
+
let exampleValue: string;
|
|
127
|
+
|
|
128
|
+
if (field.repeated) {
|
|
129
|
+
if (optionalBehavior === "all-required") {
|
|
130
|
+
const instance = createTestFieldValue(
|
|
131
|
+
{ ...field, repeated: false } as proto.Field,
|
|
132
|
+
optionalBehavior,
|
|
133
|
+
/* omitFieldName */ true,
|
|
134
|
+
);
|
|
135
|
+
exampleValue = `[${instance}, ${instance}]`;
|
|
136
|
+
} else {
|
|
137
|
+
exampleValue = "[]"; // Use empty array for repeated fields when omitting optional fields
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
switch (fieldType) {
|
|
141
|
+
case "string":
|
|
142
|
+
exampleValue =
|
|
143
|
+
optionalBehavior === "default-optional" && isOptional
|
|
144
|
+
? `""`
|
|
145
|
+
: `"example"`;
|
|
146
|
+
break;
|
|
147
|
+
case "int32":
|
|
148
|
+
case "uint32":
|
|
149
|
+
case "sint32":
|
|
150
|
+
case "fixed32":
|
|
151
|
+
case "sfixed32":
|
|
152
|
+
exampleValue =
|
|
153
|
+
optionalBehavior === "default-optional" && isOptional ? `0` : `123`;
|
|
154
|
+
break;
|
|
155
|
+
case "int64":
|
|
156
|
+
case "uint64":
|
|
157
|
+
case "sint64":
|
|
158
|
+
case "fixed64":
|
|
159
|
+
case "sfixed64":
|
|
160
|
+
exampleValue =
|
|
161
|
+
optionalBehavior === "default-optional" && isOptional ? `0n` : `123n`; // BigInt for 64-bit integers
|
|
162
|
+
break;
|
|
163
|
+
case "bool":
|
|
164
|
+
exampleValue =
|
|
165
|
+
optionalBehavior === "default-optional" && isOptional
|
|
166
|
+
? `false`
|
|
167
|
+
: `true`;
|
|
168
|
+
break;
|
|
169
|
+
case "bytes":
|
|
170
|
+
exampleValue =
|
|
171
|
+
optionalBehavior === "default-optional" && isOptional
|
|
172
|
+
? `Buffer.from([])`
|
|
173
|
+
: `Buffer.from([1, 2, 3])`;
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
const resolvedType = field.resolvedType;
|
|
177
|
+
if (resolvedType && resolvedType instanceof proto.Type) {
|
|
178
|
+
if (optionalBehavior === "default-optional" && isOptional) {
|
|
179
|
+
exampleValue = "";
|
|
180
|
+
} else {
|
|
181
|
+
exampleValue = createTestInstance(resolvedType, optionalBehavior);
|
|
182
|
+
}
|
|
183
|
+
} else if (
|
|
184
|
+
field.resolvedType &&
|
|
185
|
+
field.resolvedType instanceof proto.Enum
|
|
186
|
+
) {
|
|
187
|
+
const enumValues = Object.keys(field.resolvedType.values);
|
|
188
|
+
const middleValue = enumValues[Math.floor(enumValues.length / 2)];
|
|
189
|
+
exampleValue = `"${middleValue}"`;
|
|
190
|
+
} else {
|
|
191
|
+
logError(
|
|
192
|
+
`Unsupported field type "${fieldType}" for field "${fieldName}". Using "UNKNOWN" as placeholder.`,
|
|
193
|
+
);
|
|
194
|
+
exampleValue = "UNKNOWN";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!exampleValue) return null;
|
|
200
|
+
|
|
201
|
+
return omitFieldName
|
|
202
|
+
? `${exampleValue}`
|
|
203
|
+
: ` ${fieldName}: ${exampleValue},`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { generateNamespaceTests };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import proto from "protobufjs";
|
|
2
|
+
import {
|
|
3
|
+
lowercaseFirstLetter,
|
|
4
|
+
removeRequestSuffix,
|
|
5
|
+
} from "../utils/string-manipulation.js";
|
|
6
|
+
import {
|
|
7
|
+
getDeserializerFunctionName,
|
|
8
|
+
getSerializerFunctionName,
|
|
9
|
+
} from "./serialization.js";
|
|
10
|
+
import { getMessages } from "../utils/protobuf.js";
|
|
11
|
+
import { getMessageId } from "../utils/associations.js";
|
|
12
|
+
import { logError } from "../utils/logger.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generates wrapper functions for all messages and enums in a protobuf namespace, including nested namespaces, and returns the content of the wrapper file as a string.
|
|
16
|
+
* @param namespace - The protobuf namespace to create wrapper functions for
|
|
17
|
+
* @param associations - An array of tuples representing request-response message associations, where each tuple contains a request message type and its corresponding response message type. These associations are used to generate additional traits for request-response pairs.
|
|
18
|
+
* @returns A string containing the content of the wrapper file with all the generated wrapper functions for the protobuf namespace
|
|
19
|
+
*/
|
|
20
|
+
function createNamespaceTraits(
|
|
21
|
+
namespace: proto.Namespace,
|
|
22
|
+
associations: [proto.Type, proto.Type][],
|
|
23
|
+
): string {
|
|
24
|
+
let traitsContent = "";
|
|
25
|
+
const messageTraits: string[] = [];
|
|
26
|
+
const allImports: string[] = [];
|
|
27
|
+
const messages = getMessages(namespace);
|
|
28
|
+
for (const message of messages) {
|
|
29
|
+
const result = createMessageTraits(message);
|
|
30
|
+
if (!result) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const { definition, name, imports } = result;
|
|
34
|
+
traitsContent += definition;
|
|
35
|
+
messageTraits.push(name);
|
|
36
|
+
allImports.push(...imports);
|
|
37
|
+
}
|
|
38
|
+
const { baseName, version } = parseNamespace(namespace.name);
|
|
39
|
+
messageTraits.push("API_NAME", "API_VERSION");
|
|
40
|
+
|
|
41
|
+
let associationTraits = "";
|
|
42
|
+
for (const [request, response] of associations) {
|
|
43
|
+
const pairName = removeRequestSuffix(request.name);
|
|
44
|
+
const pairTraitsName = getReqResTraitName(pairName);
|
|
45
|
+
const requestTraitName = getTraitName(request.name);
|
|
46
|
+
const responseTraitName = getTraitName(response.name);
|
|
47
|
+
associationTraits += `const ${pairTraitsName} = {
|
|
48
|
+
name: "${pairName}",
|
|
49
|
+
request: ${requestTraitName},
|
|
50
|
+
response: ${responseTraitName},
|
|
51
|
+
};\n`;
|
|
52
|
+
if (!messageTraits.includes(requestTraitName)) {
|
|
53
|
+
logError(
|
|
54
|
+
`${request.name} is missing a message ID but is detected as a request message. Please add a 'MessageId: ###' comment to it.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (!messageTraits.includes(responseTraitName)) {
|
|
58
|
+
logError(
|
|
59
|
+
`${response.name} is missing a message ID but is detected as a response message. Please add a 'MessageId: ###' comment to it.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
messageTraits.push(pairTraitsName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `import { ${[...new Set(allImports)].sort().join(", ")} } from "./serialization.js";
|
|
66
|
+
|
|
67
|
+
const API_NAME = "${baseName}";
|
|
68
|
+
const API_VERSION = ${version};
|
|
69
|
+
${traitsContent}
|
|
70
|
+
${associationTraits}
|
|
71
|
+
export {
|
|
72
|
+
${messageTraits.sort().join(",\n ")}
|
|
73
|
+
};
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parses a protobuf namespace name to extract the base name and version number. The expected format for the namespace name is "NamespaceV1", where "Namespace" is the base name and "1" is the version number. If the namespace name does not match this format, an error is thrown.
|
|
79
|
+
* @param namespace - The protobuf namespace name to parse
|
|
80
|
+
* @returns An object containing the base name and version number extracted from the namespace name
|
|
81
|
+
* @throws An error if the namespace name does not match the expected format
|
|
82
|
+
*/
|
|
83
|
+
function parseNamespace(namespace: string) {
|
|
84
|
+
const match = namespace.match(/^(.*)_?v(\d+)$/i);
|
|
85
|
+
if (!match) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid namespace format: ${namespace}. Expected format is "NamespaceV1"`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
baseName: match[1]?.replace(/_?$/, "") || "", // Remove trailing underscore if present
|
|
92
|
+
version: match[2],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a traits object for a protobuf message type, which includes the message ID, name, and references to the corresponding serializer and deserializer functions. If the message does not have an associated message ID, the function returns null and logs an error indicating that the message is missing a message ID.
|
|
98
|
+
* @param message - The protobuf message type to create the traits object for
|
|
99
|
+
* @returns An object containing the TypeScript definition for the traits object, the name of the traits object, and an array of imports required for the serializer and deserializer functions. If the message is missing a message ID, it returns null.
|
|
100
|
+
*/
|
|
101
|
+
function createMessageTraits(message: proto.Type) {
|
|
102
|
+
const id = getMessageId(message);
|
|
103
|
+
if (!id) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const traitName = getTraitName(message.name);
|
|
107
|
+
const serialize = getSerializerFunctionName(message.name);
|
|
108
|
+
const deserialize = getDeserializerFunctionName(message.name);
|
|
109
|
+
const definition = `
|
|
110
|
+
const ${traitName} = {
|
|
111
|
+
deserialize: ${deserialize},
|
|
112
|
+
id: ${id},
|
|
113
|
+
name: "${message.name}",
|
|
114
|
+
serialize: ${serialize},
|
|
115
|
+
};\n`;
|
|
116
|
+
return {
|
|
117
|
+
definition,
|
|
118
|
+
name: traitName,
|
|
119
|
+
imports: [serialize, deserialize],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generates a trait name for a protobuf message type by converting the first letter of the message name to lowercase and appending "Traits" to the end. For example, if the message name is "MyMessage", the generated trait name would be "myMessageTraits".
|
|
125
|
+
* @param messageName - The name of the protobuf message type to generate the trait name for
|
|
126
|
+
* @returns A string containing the generated trait name for the protobuf message type
|
|
127
|
+
*/
|
|
128
|
+
function getTraitName(messageName: string) {
|
|
129
|
+
return `${lowercaseFirstLetter(messageName)}Traits`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generates a trait name for a request-response pair of protobuf message types by removing the "Request" suffix from the request message name, converting the first letter to lowercase, and appending "ReqResTraits" to the end. For example, if the request message name is "GetUserRequest", the generated trait name for the request-response pair would be "getUserReqResTraits".
|
|
134
|
+
* @param pairName - The base name of the request-response pair, which is derived from the request message name by removing the "Request" suffix
|
|
135
|
+
* @returns A string containing the generated trait name for the request-response pair of protobuf message types
|
|
136
|
+
*/
|
|
137
|
+
function getReqResTraitName(pairName: string) {
|
|
138
|
+
return `${lowercaseFirstLetter(pairName)}ReqResTraits`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { createNamespaceTraits };
|