Skip to content

Commit a49a56d

Browse files
author
infodusha
committed
feat: initial commit
1 parent eddae3b commit a49a56d

19 files changed

+6747
-0
lines changed

.eslintignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
lib/
3+
__tests__/test-data/
4+
5+
.eslintrc.js
6+
jest.config.js

.eslintrc.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
es2021: true,
5+
jest: true,
6+
},
7+
extends: [
8+
'eslint:recommended',
9+
'plugin:@typescript-eslint/recommended',
10+
],
11+
parser: '@typescript-eslint/parser',
12+
parserOptions: {
13+
ecmaVersion: 'latest',
14+
sourceType: 'module',
15+
},
16+
plugins: [
17+
'@typescript-eslint',
18+
],
19+
rules: {
20+
semi: ['error', 'always'],
21+
'no-extra-semi': 'error',
22+
'comma-dangle': ['error', 'always-multiline'],
23+
'space-before-function-paren': ['error', {
24+
anonymous: 'never',
25+
named: 'never',
26+
asyncArrow: 'always',
27+
}],
28+
},
29+
};

.github/workflows/merge.yml

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Merge
2+
on:
3+
push:
4+
branches:
5+
- main
6+
jobs:
7+
release:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
id-token: write
13+
steps:
14+
- uses: google-github-actions/release-please-action@v3
15+
id: release
16+
with:
17+
release-type: node
18+
pull-request-title-pattern: 'chore: release ${version}'
19+
- uses: actions/checkout@v3
20+
if: ${{ steps.release.outputs.release_created }}
21+
- uses: actions/setup-node@v3
22+
with:
23+
node-version: '20.x'
24+
registry-url: 'https://registry.npmjs.org'
25+
if: ${{ steps.release.outputs.release_created }}
26+
- name: Install Protobuf
27+
uses: arduino/setup-protoc@v1
28+
with:
29+
version: '3.x'
30+
if: ${{ steps.release.outputs.release_created }}
31+
- run: npm install -g npm
32+
if: ${{ steps.release.outputs.release_created }}
33+
- run: npm ci
34+
if: ${{ steps.release.outputs.release_created }}
35+
- run: npm run generate
36+
if: ${{ steps.release.outputs.release_created }}
37+
- run: npm run build
38+
if: ${{ steps.release.outputs.release_created }}
39+
- run: npm run test
40+
if: ${{ steps.release.outputs.release_created }}
41+
- run: npm publish --provenance
42+
env:
43+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
44+
if: ${{ steps.release.outputs.release_created }}

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.idea/
2+
.vscode/
3+
coverage/
4+
node_modules/
5+
.npm/
6+
.eslintcache/
7+
lib/
8+
9+
__tests__/test-data/generated/

.release-please-manifest.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
".": "1.0.0"
3+
}

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# from-protobuf-object
2+
fromObject method for grpc-web
3+
4+
In general that is opposite for 'toObject' method in protobufjs.
5+
6+
### Supports:
7+
* Simple keys
8+
* Repeated
9+
* OneOf
10+
* Protobuf Map
11+
* Recursive messages
12+
* Type validation (at runtime)
13+
* TypeScript
14+
* Missing keys validation
15+
16+
## Installation
17+
`npm i from-protobuf-object`
18+
19+
## Usage
20+
```typescript
21+
import { fromProtobufObject } from 'from-protobuf-object';
22+
import { MyMessage } from './my-message_pb';
23+
24+
const myMessage = fromProtobufObject(MyMessage, {
25+
keyOne: 1,
26+
keyTwo: 'foo',
27+
keyThree: {
28+
keyA: 2,
29+
keyB: 'bar',
30+
},
31+
});
32+
```
33+
34+
## Contributing
35+
Contributions are always welcome!
36+
37+
## License
38+
[Apache-2.0](https://choosealicense.com/licenses/apache-2.0/)

__tests__/index.test.ts

+248
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { fromProtobufObject } from "../src";
2+
import { BookStore } from "./test-data/generated/book-store_pb";
3+
import { PhoneShop } from "./test-data/generated/phone-shop_pb";
4+
import { Forest } from "./test-data/generated/forest_pb";
5+
import { Universe } from "./test-data/generated/universe_pb";
6+
import { Spices } from "./test-data/generated/spices_pb";
7+
import { Newspaper } from "./test-data/generated/newspaper_pb";
8+
9+
describe('fromProtobufObject', () => {
10+
it('Should work with easy structure', () => {
11+
const obj = {
12+
name: 'Harry Potter',
13+
shelf: 3,
14+
} satisfies BookStore.AsObject;
15+
const bookStore = fromProtobufObject(BookStore, obj);
16+
expect(bookStore.toObject()).toEqual(obj);
17+
});
18+
19+
it('Should work with nested structure', () => {
20+
const obj = {
21+
id: 1,
22+
phone: {
23+
model: 'iPhone 11',
24+
diagonal: 6,
25+
price: 999,
26+
company: {
27+
name: 'Apple',
28+
country: 'USA',
29+
},
30+
},
31+
} satisfies PhoneShop.AsObject;
32+
const phoneShop = fromProtobufObject(PhoneShop, obj);
33+
expect(phoneShop.toObject()).toEqual(obj);
34+
});
35+
36+
it('Should ignore extra params', () => {
37+
const planetsList = ['Earth', 'Mars', 'Venus'];
38+
const obj = {
39+
planetsList,
40+
extra: 'data',
41+
} as Universe.AsObject;
42+
const universe = fromProtobufObject(Universe, obj);
43+
expect(universe.toObject()).toEqual({ planetsList } satisfies Universe.AsObject);
44+
});
45+
46+
it('Should throw when lack of params', () => {
47+
const obj = {
48+
name: 'Esperanto',
49+
} as BookStore.AsObject;
50+
expect(() => fromProtobufObject(BookStore, obj)).toThrowError(`Missing property 'shelf'`);
51+
});
52+
53+
it('Should not throw when lack of array params', () => {
54+
const obj = {} as Universe.AsObject;
55+
expect(() => fromProtobufObject(Universe, obj)).not.toThrowError(`Missing property 'planetsList'`);
56+
expect(() => fromProtobufObject(Universe, obj)).not.toThrow();
57+
});
58+
59+
it('Should throw when null value', () => {
60+
const obj = {
61+
planetsList: null,
62+
} as unknown as Universe.AsObject;
63+
expect(() => fromProtobufObject(Universe, obj)).toThrowError(`Null value for key 'planetsList'`);
64+
});
65+
66+
it('Should not throw when extra null value', () => {
67+
const obj = {
68+
planetsList: ['Saturn'],
69+
extra: null,
70+
} as Universe.AsObject;
71+
expect(() => fromProtobufObject(Universe, obj)).not.toThrowError(`Null value for key 'extra'`);
72+
expect(() => fromProtobufObject(Universe, obj)).not.toThrow();
73+
});
74+
});
75+
76+
describe('Oneof rule', () => {
77+
it('Should work with oneof rule', () => {
78+
const objNews = {
79+
id: 1,
80+
name: 'The New York Times',
81+
isAds: true,
82+
isNews: true,
83+
contentByPageMap: [],
84+
adsByPageMap: [],
85+
} satisfies Newspaper.AsObject;
86+
const objAds = {
87+
id: 2,
88+
name: 'RBK',
89+
isNews: true,
90+
isAds: true,
91+
contentByPageMap: [],
92+
adsByPageMap: [],
93+
} satisfies Newspaper.AsObject;
94+
const newspaperNews = fromProtobufObject(Newspaper, objNews);
95+
const newspaperAds = fromProtobufObject(Newspaper, objAds);
96+
expect(newspaperNews.toObject()).toEqual({ ...objNews, isAds: false });
97+
expect(newspaperAds.toObject()).toEqual({ ...objAds, isNews: false });
98+
});
99+
});
100+
101+
describe('Validation', () => {
102+
it('Should validate simple type', () => {
103+
const obj = {
104+
name: 'Harry Potter',
105+
shelf: 'second',
106+
} as unknown as BookStore.AsObject;
107+
expect(() => fromProtobufObject(BookStore, obj)).toThrowError(`Invalid type for 'shelf' (expected 'number', got 'string')`);
108+
});
109+
110+
it('Should validate nested structure', () => {
111+
const obj = {
112+
id: 1,
113+
phone: 123,
114+
} as unknown as PhoneShop.AsObject;
115+
expect(() => fromProtobufObject(PhoneShop, obj)).toThrowError(`Invalid type for 'phone' (expected 'object', got 'number')`);
116+
});
117+
118+
it('Should not throw on undefined objects', () => {
119+
const obj = {
120+
id: 1,
121+
phone: undefined,
122+
} satisfies PhoneShop.AsObject;
123+
expect(() => fromProtobufObject(PhoneShop, obj)).not.toThrow();
124+
});
125+
126+
it('Should validate simple array', () => {
127+
const obj = {
128+
planetsList: true,
129+
} as unknown as Universe.AsObject;
130+
expect(() => fromProtobufObject(Universe, obj)).toThrowError(`Invalid type for 'planetsList' (expected array, got 'boolean')`);
131+
});
132+
133+
it('Should validate simple false array', () => {
134+
const obj = {
135+
name: 'Gary Garrison',
136+
shelf: [4, 2],
137+
} as unknown as BookStore.AsObject;
138+
expect(() => fromProtobufObject(BookStore, obj)).toThrowError(`Invalid type for 'shelf' (expected 'number', got array)`);
139+
});
140+
});
141+
142+
describe('Repeated rule', () => {
143+
it('Should work with simple array', () => {
144+
const obj = {
145+
planetsList: ['Earth', 'Mars', 'Venus'],
146+
} satisfies Universe.AsObject;
147+
const universe = fromProtobufObject(Universe, obj);
148+
expect(universe.toObject()).toEqual(obj);
149+
});
150+
151+
it('Should work with empty array', () => {
152+
const obj = {
153+
planetsList: [],
154+
} satisfies Universe.AsObject;
155+
const universe = fromProtobufObject(Universe, obj);
156+
expect(universe.toObject()).toEqual(obj);
157+
});
158+
159+
160+
it('Should work with array structure', () => {
161+
const obj = {
162+
treesList: [
163+
{ age: 1, height: 10 },
164+
{ age: 5, height: 42 },
165+
],
166+
info: {
167+
name: 'Forest',
168+
numberOfTrees: 4000,
169+
},
170+
} satisfies Forest.AsObject;
171+
const forest = fromProtobufObject(Forest, obj);
172+
expect(forest.toObject()).toEqual(obj);
173+
});
174+
175+
it('Should throw when null in array', () => {
176+
const obj = {
177+
planetsList: [null],
178+
} as unknown as Universe.AsObject;
179+
expect(() => fromProtobufObject(Universe, obj)).toThrowError(`Null value for key 'planetsList'`);
180+
});
181+
182+
it('Should throw when mixed array', () => {
183+
const obj = {
184+
planetsList: ['Saturn', {}],
185+
} as Universe.AsObject;
186+
expect(() => fromProtobufObject(Universe, obj)).toThrowError(`Mixed array for 'planetsList'`);
187+
});
188+
});
189+
190+
describe('Recursive messages', () => {
191+
it('Should work with recursive', () => {
192+
const obj = {
193+
name: 'Pork',
194+
mixed: {
195+
spicesList: [
196+
{ name: 'Pepper' },
197+
{
198+
name: 'Salt',
199+
mixed: {
200+
spicesList: [{
201+
name: 'Dark',
202+
mixed: { spicesList: [{ name: 'Void' }] },
203+
}],
204+
},
205+
},
206+
],
207+
},
208+
} satisfies Spices.AsObject;
209+
const spices = fromProtobufObject(Spices, obj);
210+
expect(spices.toObject()).toEqual(obj);
211+
});
212+
});
213+
214+
describe('Map rule', () => {
215+
it('Should work with simple map', () => {
216+
const obj = {
217+
id: 1,
218+
name: 'The New York Times',
219+
isAds: false,
220+
isNews: true,
221+
contentByPageMap: [
222+
[1, 'fist page'],
223+
[2, 'second page'],
224+
],
225+
adsByPageMap: [],
226+
} satisfies Newspaper.AsObject;
227+
228+
const newspaper = fromProtobufObject(Newspaper, obj);
229+
expect(newspaper.toObject()).toEqual(obj);
230+
});
231+
232+
it('Should work with nested type', () => {
233+
const obj = {
234+
id: 1,
235+
name: 'The New York Times',
236+
isAds: false,
237+
isNews: true,
238+
contentByPageMap: [],
239+
adsByPageMap: [
240+
[1, { data: 'Google' }],
241+
[2, { data: 'Facebook' }],
242+
],
243+
} satisfies Newspaper.AsObject;
244+
245+
const newspaper = fromProtobufObject(Newspaper, obj);
246+
expect(newspaper.toObject()).toEqual(obj);
247+
});
248+
});

__tests__/test-data/book-store.proto

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
syntax = "proto3";
2+
3+
message BookStore {
4+
string name = 1;
5+
int32 shelf = 2;
6+
}

0 commit comments

Comments
 (0)