Skip to content

Commit cf689e8

Browse files
authored
feat: implement json to python conversion (#18)
* feat: implement json to python convertion
1 parent d63d34c commit cf689e8

File tree

7 files changed

+483
-7
lines changed

7 files changed

+483
-7
lines changed

examples/example.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import List, TypedDict
2+
3+
4+
class SubStruct1(TypedDict):
5+
key: str
6+
7+
8+
class GeneratedStruct(TypedDict):
9+
array_key: List[int]
10+
boolean_key: bool
11+
map_key: SubStruct1
12+
number_key: int
13+
string_key: str

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "json2struct",
33
"version": "0.2.0",
4-
"description": "CLI tool for converting JSON files into TypeScript types",
4+
"description": "Tool for converting JSON to TypeScript & Python interfaces",
55
"main": "dist/index.js",
66
"bin": {
77
"json2struct": "./dist/index.js"
@@ -14,14 +14,20 @@
1414
"lint": "eslint --ext \".js,.mjs,.ts,.d.ts\" --ignore-path .gitignore .",
1515
"test": "vitest --run",
1616
"test:watch": "vitest",
17-
"local": "npm uninstall -g && npm install -g && json2struct"
17+
"local": "npm uninstall -g && npm install -g && json2struct",
18+
"example:typescript": "node dist/index.js ./examples/example.json ./examples/example.d.ts --language typescript --overwrite",
19+
"example:python": "node dist/index.js ./examples/example.json ./examples/example.py --language python --overwrite"
1820
},
1921
"repository": {
2022
"type": "git",
2123
"url": "git+https://github.com/hougesen/json2struct.git"
2224
},
2325
"keywords": [
24-
"json"
26+
"json",
27+
"types",
28+
"cli",
29+
"typescript",
30+
"python"
2531
],
2632
"author": "Mads Hougesen",
2733
"license": "MIT",
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { convertTokenToPython, generatePythonStruct } from '../../languages/python';
4+
import { tokenize } from '../../tokenizer/';
5+
6+
describe('primitives', () => {
7+
it('strings', () => {
8+
expect(convertTokenToPython(tokenize('mads'), new Set(), new Map())).toEqual('str');
9+
expect(generatePythonStruct(tokenize('mads'))).toEqual('GeneratedStruct: str\n');
10+
11+
expect(convertTokenToPython(tokenize('was'), new Set(), new Map())).toEqual('str');
12+
expect(generatePythonStruct(tokenize('was'))).toEqual('GeneratedStruct: str\n');
13+
14+
expect(convertTokenToPython(tokenize('here'), new Set(), new Map())).toEqual('str');
15+
16+
expect(generatePythonStruct(tokenize('here'))).toEqual('GeneratedStruct: str\n');
17+
});
18+
19+
it('numbers', () => {
20+
expect(convertTokenToPython(tokenize(1), new Set(), new Map())).toEqual('int');
21+
expect(generatePythonStruct(tokenize(1))).toEqual('GeneratedStruct: int\n');
22+
23+
expect(convertTokenToPython(tokenize(2), new Set(), new Map())).toEqual('int');
24+
expect(generatePythonStruct(tokenize(2))).toEqual('GeneratedStruct: int\n');
25+
26+
expect(convertTokenToPython(tokenize(3), new Set(), new Map())).toEqual('int');
27+
expect(generatePythonStruct(tokenize(3))).toEqual('GeneratedStruct: int\n');
28+
});
29+
30+
it('floats', () => {
31+
expect(convertTokenToPython(tokenize(1.2), new Set(), new Map())).toEqual('float');
32+
expect(generatePythonStruct(tokenize(1.2))).toEqual('GeneratedStruct: float\n');
33+
34+
expect(convertTokenToPython(tokenize(3.21), new Set(), new Map())).toEqual('float');
35+
expect(generatePythonStruct(tokenize(3.21))).toEqual('GeneratedStruct: float\n');
36+
});
37+
38+
it('booleans', () => {
39+
expect(convertTokenToPython(tokenize(true), new Set(), new Map())).toEqual('bool');
40+
expect(generatePythonStruct(tokenize(true))).toEqual('GeneratedStruct: bool\n');
41+
42+
expect(convertTokenToPython(tokenize(false), new Set(), new Map())).toEqual('bool');
43+
expect(generatePythonStruct(tokenize(false))).toEqual('GeneratedStruct: bool\n');
44+
});
45+
46+
it('nulls', () => {
47+
expect(convertTokenToPython(tokenize(null), new Set(), new Map())).toEqual('None');
48+
expect(generatePythonStruct(tokenize(null))).toEqual('GeneratedStruct: None\n');
49+
});
50+
});
51+
52+
describe('arrays', () => {
53+
it('empty arrays should be List[Any]', () => {
54+
expect(convertTokenToPython(tokenize([]), new Set(), new Map())).toEqual('List[Any]');
55+
expect(generatePythonStruct(tokenize([]))).toEqual(
56+
'from typing import Any, List\n\n\nGeneratedStruct: List[Any]\n'
57+
);
58+
});
59+
60+
it('it should be possible to nest arrays', () => {
61+
expect(convertTokenToPython(tokenize([[]]), new Set(), new Map())).toEqual('List[List[Any]]');
62+
63+
expect(convertTokenToPython(tokenize([[[]]]), new Set(), new Map())).toEqual('List[List[List[Any]]]');
64+
65+
expect(convertTokenToPython(tokenize([[['mhouge.dk']]]), new Set(), new Map())).toEqual(
66+
'List[List[List[str]]]'
67+
);
68+
69+
expect(convertTokenToPython(tokenize([[[1.2]]]), new Set(), new Map())).toEqual('List[List[List[float]]]');
70+
71+
expect(convertTokenToPython(tokenize([[[1]]]), new Set(), new Map())).toEqual('List[List[List[int]]]');
72+
73+
expect(convertTokenToPython(tokenize([[[{}]]]), new Set(), new Map())).toEqual(
74+
'List[List[List[Dict[Any, Any]]]]'
75+
);
76+
});
77+
78+
it('duplicate primitives should be removed from arrays', () => {
79+
expect(convertTokenToPython(tokenize(['mads', 'was', 'here']), new Set(), new Map())).toEqual('List[str]');
80+
81+
expect(generatePythonStruct(tokenize(['mads', 'was', 'here']))).toEqual(
82+
'from typing import List\n\n\nGeneratedStruct: List[str]\n'
83+
);
84+
85+
expect(convertTokenToPython(tokenize([1, 2, 3]), new Set(), new Map())).toEqual('List[int]');
86+
87+
expect(generatePythonStruct(tokenize([1, 2, 3]))).toEqual(
88+
'from typing import List\n\n\nGeneratedStruct: List[int]\n'
89+
);
90+
});
91+
92+
it('arrays should support multiple types', () => {
93+
expect(convertTokenToPython(tokenize(['mads', 1, 'mhouge.dk', 2, 3]), new Set(), new Map())).toEqual(
94+
'List[Union[int, str]]'
95+
);
96+
97+
expect(generatePythonStruct(tokenize(['mads', 1, 'mhouge.dk', 2, 3]))).toEqual(
98+
'from typing import List, Union\n\n\nGeneratedStruct: List[Union[int, str]]\n'
99+
);
100+
});
101+
102+
it('duplicate maps should be removed from arrays', () => {
103+
expect(
104+
convertTokenToPython(tokenize([{ key: 'mads' }, { key: 'was' }, { key: 'here' }]), new Set(), new Map())
105+
).toEqual('List[SubStruct1]');
106+
107+
expect(generatePythonStruct(tokenize([{ key: 'mads' }, { key: 'was' }, { key: 'here' }]))).toEqual(
108+
`from typing import List, TypedDict
109+
110+
111+
class SubStruct1(TypedDict):
112+
key: str
113+
114+
115+
GeneratedStruct: List[SubStruct1]
116+
`
117+
);
118+
});
119+
120+
it('maps should be able to be mixed in arrays', () => {
121+
expect(
122+
convertTokenToPython(tokenize([{ key: 1.23 }, { key: 'mads' }, { key: 1 }]), new Set(), new Map())
123+
).toEqual('List[Union[SubStruct1, SubStruct2, SubStruct3]]');
124+
});
125+
});
126+
127+
describe('maps', () => {
128+
it('empty maps should be Dict[Any, Any]', () => {
129+
expect(convertTokenToPython(tokenize({}), new Set(), new Map())).toEqual('Dict[Any, Any]');
130+
131+
expect(generatePythonStruct(tokenize({}))).toEqual(
132+
'from typing import Any, Dict\n\n\nGeneratedStruct: Dict[Any, Any]\n'
133+
);
134+
});
135+
136+
it('maps should support primitive value children', () => {
137+
expect(generatePythonStruct(tokenize({ key: 'value' }))).toEqual(
138+
`from typing import TypedDict
139+
140+
141+
class GeneratedStruct(TypedDict):
142+
key: str
143+
`
144+
);
145+
146+
expect(
147+
generatePythonStruct(
148+
tokenize({
149+
stringKey: 'value',
150+
numberKey: 1,
151+
nullKey: null,
152+
trueKey: true,
153+
falseKey: false,
154+
})
155+
)
156+
).toEqual(
157+
`from typing import TypedDict
158+
159+
160+
class GeneratedStruct(TypedDict):
161+
falseKey: bool
162+
nullKey: None
163+
numberKey: int
164+
stringKey: str
165+
trueKey: bool
166+
`
167+
);
168+
});
169+
170+
it('maps should be able to be nested', () => {
171+
const expectedResult = `from typing import TypedDict
172+
173+
174+
class SubStruct1(TypedDict):
175+
key: str
176+
177+
178+
class SubStruct2(TypedDict):
179+
d: SubStruct1
180+
181+
182+
class SubStruct3(TypedDict):
183+
c: SubStruct2
184+
185+
186+
class SubStruct4(TypedDict):
187+
b: SubStruct3
188+
189+
190+
class GeneratedStruct(TypedDict):
191+
a: SubStruct4
192+
`;
193+
expect(
194+
generatePythonStruct(
195+
tokenize({
196+
a: {
197+
b: {
198+
c: {
199+
d: {
200+
key: 'value',
201+
},
202+
},
203+
},
204+
},
205+
})
206+
)
207+
).toEqual(expectedResult);
208+
});
209+
210+
it('it should be possible to mix map with arrays', async () => {
211+
expect(generatePythonStruct(tokenize({ arr: [1.23] }))).toEqual(
212+
'from typing import List, TypedDict\n\n\nclass GeneratedStruct(TypedDict):\n arr: List[float]\n'
213+
);
214+
});
215+
216+
it('maps should be sorted automatically', () => {
217+
expect(generatePythonStruct(tokenize({ a: 'a', b: 'b', c: 'c' }))).toEqual(
218+
generatePythonStruct(tokenize({ c: 'c', b: 'b', a: 'a' }))
219+
);
220+
});
221+
});
222+
223+
describe('generatePythonStruct', () => {
224+
describe('base types', () => {
225+
it('only string', () => expect(generatePythonStruct(tokenize('mhouge.dk'))).toEqual('GeneratedStruct: str\n'));
226+
227+
it('only number', () => expect(generatePythonStruct(tokenize(42))).toEqual('GeneratedStruct: int\n'));
228+
229+
it('only float', () => expect(generatePythonStruct(tokenize(42.42))).toEqual('GeneratedStruct: float\n'));
230+
231+
it('only null', () => expect(generatePythonStruct(tokenize(null))).toEqual('GeneratedStruct: None\n'));
232+
233+
it('empty array', () =>
234+
expect(generatePythonStruct(tokenize([]))).toEqual(
235+
'from typing import Any, List\n\n\nGeneratedStruct: List[Any]\n'
236+
));
237+
238+
it('string array', () =>
239+
expect(generatePythonStruct(tokenize(['mhouge.dk']))).toEqual(
240+
'from typing import List\n\n\nGeneratedStruct: List[str]\n'
241+
));
242+
243+
it('number array', () =>
244+
expect(generatePythonStruct(tokenize([42]))).toEqual(
245+
'from typing import List\n\n\nGeneratedStruct: List[int]\n'
246+
));
247+
248+
// NOTE: should this be switched to Array<unknown>?
249+
it('null array', () =>
250+
expect(generatePythonStruct(tokenize([null]))).toEqual(
251+
'from typing import List\n\n\nGeneratedStruct: List[None]\n'
252+
));
253+
254+
it('empty matrix', () =>
255+
expect(generatePythonStruct(tokenize([[], [], []]))).toEqual(
256+
'from typing import Any, List\n\n\nGeneratedStruct: List[List[Any]]\n'
257+
));
258+
259+
it('mixed array', () => {
260+
expect(generatePythonStruct(tokenize([1, 'mhouge.dk']))).toEqual(
261+
'from typing import List, Union\n\n\nGeneratedStruct: List[Union[int, str]]\n'
262+
);
263+
264+
expect(generatePythonStruct(tokenize([1, 'mhouge.dk', null]))).toEqual(
265+
'from typing import List, Union\n\n\nGeneratedStruct: List[Union[None, int, str]]\n'
266+
);
267+
});
268+
});
269+
270+
describe('objects', () => {
271+
it('empty dict', () =>
272+
expect(generatePythonStruct(tokenize({}))).toEqual(
273+
'from typing import Any, Dict\n\n\nGeneratedStruct: Dict[Any, Any]\n'
274+
));
275+
276+
it('object with only primitives', () => {
277+
const jsonStr = `
278+
{
279+
"tabWidth": 4,
280+
"useTabs": false,
281+
"printWidth": 120,
282+
"singleQuote": true,
283+
"semi": true
284+
}`;
285+
286+
const expectedResult =
287+
'from typing import TypedDict\n\n\nclass GeneratedStruct(TypedDict):\n printWidth: int\n semi: bool\n singleQuote: bool\n tabWidth: int\n useTabs: bool\n';
288+
289+
expect(generatePythonStruct(tokenize(JSON.parse(jsonStr)))).toEqual(expectedResult);
290+
});
291+
292+
it('mixed record', () => {
293+
const jsonStr = `
294+
{
295+
"data": [
296+
{
297+
"length" : 60,
298+
"message" : "",
299+
"retry_after" : 480
300+
}
301+
]
302+
}`;
303+
304+
const expectedResult = `from typing import List, TypedDict
305+
306+
307+
class SubStruct1(TypedDict):
308+
length: int
309+
message: str
310+
retry_after: int
311+
312+
313+
class GeneratedStruct(TypedDict):
314+
data: List[SubStruct1]
315+
`;
316+
317+
expect(generatePythonStruct(tokenize(JSON.parse(jsonStr)))).toEqual(expectedResult);
318+
});
319+
});
320+
});

src/__tests__/languages/typescript.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ describe('maps', () => {
131131
});
132132
});
133133

134-
describe('json2ts', async () => {
135-
describe('arrays', () => {
134+
describe('generateTypeScriptType', () => {
135+
describe('base types', () => {
136136
it('only string', () =>
137137
expect(generateTypeScriptType(tokenize('mhouge.dk'))).toEqual('type GeneratedStruct = string'));
138138

0 commit comments

Comments
 (0)