ts-backend-check 1.2.2__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.
- ts_backend_check/checker.py +209 -54
- ts_backend_check/cli/generate_config_file.py +235 -0
- ts_backend_check/cli/generate_test_project.py +54 -0
- ts_backend_check/cli/main.py +212 -51
- ts_backend_check/parsers/django_parser.py +22 -7
- ts_backend_check/parsers/typescript_parser.py +46 -14
- ts_backend_check/test_project/backend/models.py +31 -0
- ts_backend_check/test_project/frontend/invalid_interfaces.ts +21 -0
- ts_backend_check/test_project/frontend/valid_interfaces.ts +18 -0
- ts_backend_check/utils.py +25 -1
- {ts_backend_check-1.2.2.dist-info → ts_backend_check-1.3.0.dist-info}/METADATA +175 -56
- ts_backend_check-1.3.0.dist-info/RECORD +21 -0
- {ts_backend_check-1.2.2.dist-info → ts_backend_check-1.3.0.dist-info}/WHEEL +1 -1
- ts_backend_check/cli/check_blank.py +0 -111
- ts_backend_check/cli/config.py +0 -164
- ts_backend_check/django_parser.py +0 -113
- ts_backend_check/typescript_parser.py +0 -105
- ts_backend_check-1.2.2.data/data/requirements.txt +0 -26
- ts_backend_check-1.2.2.dist-info/RECORD +0 -21
- {ts_backend_check-1.2.2.dist-info → ts_backend_check-1.3.0.dist-info}/entry_points.txt +0 -0
- {ts_backend_check-1.2.2.dist-info → ts_backend_check-1.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {ts_backend_check-1.2.2.dist-info → ts_backend_check-1.3.0.dist-info}/top_level.txt +0 -0
ts_backend_check/checker.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
Main module for checking Django models against TypeScript types.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Dict, List,
|
|
6
|
+
from typing import Dict, List, Tuple
|
|
7
7
|
|
|
8
|
-
from ts_backend_check.parsers.django_parser import
|
|
8
|
+
from ts_backend_check.parsers.django_parser import (
|
|
9
|
+
DjangoModelVisitor,
|
|
10
|
+
extract_model_fields,
|
|
11
|
+
)
|
|
9
12
|
from ts_backend_check.parsers.typescript_parser import TypeScriptParser
|
|
10
|
-
from ts_backend_check.utils import snake_to_camel
|
|
13
|
+
from ts_backend_check.utils import is_ordered_subset, snake_to_camel
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class TypeChecker:
|
|
@@ -20,13 +23,30 @@ class TypeChecker:
|
|
|
20
23
|
The file path for the models file to check.
|
|
21
24
|
|
|
22
25
|
types_file : str
|
|
23
|
-
The file path for the TypeScript file to check.
|
|
26
|
+
The file path for the TypeScript interfaces file to check.
|
|
27
|
+
|
|
28
|
+
check_blank : bool, default=False
|
|
29
|
+
Whether to also check that fields marked 'blank=True' within Django models are optional (?) in the TypeScript interfaces.
|
|
30
|
+
|
|
31
|
+
model_name_conversions : dict[str: list[str]], default={}
|
|
32
|
+
A dictionary containing conversions of model names to their corresponding TypeScript interfaces.
|
|
24
33
|
"""
|
|
25
34
|
|
|
26
|
-
def __init__(
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
models_file: str,
|
|
38
|
+
types_file: str,
|
|
39
|
+
check_blank: bool = False,
|
|
40
|
+
model_name_conversions: dict[str, list[str]] = {},
|
|
41
|
+
) -> None:
|
|
27
42
|
self.models_file = models_file
|
|
28
43
|
self.types_file = types_file
|
|
29
|
-
self.
|
|
44
|
+
self.check_blank = check_blank
|
|
45
|
+
self.model_name_conversions = model_name_conversions
|
|
46
|
+
self.django_model_visitor = DjangoModelVisitor
|
|
47
|
+
self.model_fields, self.models_and_blank_fields = extract_model_fields(
|
|
48
|
+
models_file
|
|
49
|
+
)
|
|
30
50
|
self.ts_parser = TypeScriptParser(types_file)
|
|
31
51
|
self.ts_interfaces = self.ts_parser.parse_interfaces()
|
|
32
52
|
self.backend_only = self.ts_parser.get_ignored_fields()
|
|
@@ -40,26 +60,56 @@ class TypeChecker:
|
|
|
40
60
|
list
|
|
41
61
|
A list of fields missing from the TypeScript file.
|
|
42
62
|
"""
|
|
43
|
-
|
|
63
|
+
error_fields: list[str] = []
|
|
44
64
|
|
|
45
65
|
for model_name, fields in self.model_fields.items():
|
|
46
|
-
|
|
66
|
+
missing_fields_exist = False
|
|
67
|
+
interfaces, _ = self._find_matching_interfaces(model_name=model_name)
|
|
47
68
|
|
|
48
69
|
if not interfaces:
|
|
49
|
-
|
|
50
|
-
self._format_missing_interface_message(model_name)
|
|
70
|
+
error_fields.append(
|
|
71
|
+
self._format_missing_interface_message(model_name=model_name)
|
|
51
72
|
)
|
|
52
73
|
continue
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
for field in fields:
|
|
76
|
+
if not self._field_is_accounted_for(field=field, interfaces=interfaces):
|
|
77
|
+
error_fields.append(
|
|
78
|
+
self._format_missing_field_message(
|
|
79
|
+
field=field, model_name=model_name, interfaces=interfaces
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
missing_fields_exist = True
|
|
83
|
+
|
|
84
|
+
if self.check_blank and model_name in self.models_and_blank_fields:
|
|
85
|
+
error_fields.extend(
|
|
86
|
+
self._format_optional_properties_message(
|
|
87
|
+
field=field,
|
|
88
|
+
model_name=model_name,
|
|
89
|
+
models_file=self.models_file,
|
|
90
|
+
types_file=self.types_file,
|
|
91
|
+
)
|
|
92
|
+
for field in self.models_and_blank_fields[model_name]
|
|
93
|
+
if not self._property_is_optional_when_field_is_blank(
|
|
94
|
+
model_name=model_name,
|
|
95
|
+
field=field,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not missing_fields_exist and not self._ts_interface_properties_ordered(
|
|
100
|
+
model_name=model_name, fields=fields
|
|
101
|
+
):
|
|
102
|
+
error_fields.append(
|
|
103
|
+
self._format_unordered_interface_properties_message(
|
|
104
|
+
models_file=self.models_file, types_file=self.types_file
|
|
105
|
+
)
|
|
106
|
+
)
|
|
59
107
|
|
|
60
|
-
return
|
|
108
|
+
return error_fields
|
|
61
109
|
|
|
62
|
-
def _find_matching_interfaces(
|
|
110
|
+
def _find_matching_interfaces(
|
|
111
|
+
self, model_name: str
|
|
112
|
+
) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:
|
|
63
113
|
"""
|
|
64
114
|
Find matching TypeScript interfaces for a model.
|
|
65
115
|
|
|
@@ -70,44 +120,30 @@ class TypeChecker:
|
|
|
70
120
|
|
|
71
121
|
Returns
|
|
72
122
|
-------
|
|
73
|
-
Dict[str,
|
|
123
|
+
Tuple[Dict[str, List[str]], Dict[str, List[str]]]
|
|
74
124
|
Interfaces that match a model name.
|
|
75
125
|
"""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
if self.model_name_conversions and model_name in self.model_name_conversions:
|
|
127
|
+
potential_names = self.model_name_conversions[model_name]
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
potential_names = [model_name]
|
|
131
|
+
|
|
132
|
+
interfaces = {
|
|
133
|
+
name: interface.properties
|
|
134
|
+
for name, interface in self.ts_interfaces.items()
|
|
135
|
+
if any(potential == name for potential in potential_names)
|
|
136
|
+
}
|
|
137
|
+
interfaces_with_optional_properties = {
|
|
138
|
+
name: interface.optional_properties
|
|
79
139
|
for name, interface in self.ts_interfaces.items()
|
|
80
140
|
if any(potential == name for potential in potential_names)
|
|
81
141
|
}
|
|
82
142
|
|
|
83
|
-
|
|
84
|
-
def _generate_potential_names(model_name: str) -> List[str]:
|
|
85
|
-
"""
|
|
86
|
-
Generate potential TypeScript interface names for a model.
|
|
87
|
-
|
|
88
|
-
Parameters
|
|
89
|
-
----------
|
|
90
|
-
model_name : str
|
|
91
|
-
The name of the model to check the frontend TypeScript file for.
|
|
92
|
-
|
|
93
|
-
Returns
|
|
94
|
-
-------
|
|
95
|
-
List[str]
|
|
96
|
-
Possible names for the model to check for.
|
|
97
|
-
"""
|
|
98
|
-
base_names = [
|
|
99
|
-
model_name,
|
|
100
|
-
model_name.replace("Model", ""),
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
if "_" in model_name:
|
|
104
|
-
base_names.append(snake_to_camel(input_str=model_name))
|
|
105
|
-
|
|
106
|
-
suffixes = ["", "Base", "Response", "Type"]
|
|
107
|
-
return [f"{base}{suffix}" for base in base_names for suffix in suffixes]
|
|
143
|
+
return interfaces, interfaces_with_optional_properties
|
|
108
144
|
|
|
109
|
-
def
|
|
110
|
-
self, field: str, interfaces: Dict[str,
|
|
145
|
+
def _field_is_accounted_for(
|
|
146
|
+
self, field: str, interfaces: Dict[str, List[str]]
|
|
111
147
|
) -> bool:
|
|
112
148
|
"""
|
|
113
149
|
Check if a field is accounted for in TypeScript.
|
|
@@ -117,7 +153,7 @@ class TypeChecker:
|
|
|
117
153
|
field : str
|
|
118
154
|
The field that should be used in the frontend TypeScript file.
|
|
119
155
|
|
|
120
|
-
interfaces : Dict[str,
|
|
156
|
+
interfaces : Dict[str, List[str]]
|
|
121
157
|
The interfaces from the frontend TypeScript file.
|
|
122
158
|
|
|
123
159
|
Returns
|
|
@@ -132,6 +168,64 @@ class TypeChecker:
|
|
|
132
168
|
or any(camel_field in fields for fields in interfaces.values())
|
|
133
169
|
)
|
|
134
170
|
|
|
171
|
+
def _property_is_optional_when_field_is_blank(
|
|
172
|
+
self, model_name: str, field: str
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Check that if the field is 'blank=True' that the corresponding interface property is optional (?).
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
model_name : str
|
|
180
|
+
The name of the model to check the frontend TypeScript file for.
|
|
181
|
+
|
|
182
|
+
field : str
|
|
183
|
+
The field that should match the optional state of the property in the TypeScript file.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
Bool
|
|
188
|
+
Whether the blank status of the model field matches the optional status of the interface property.
|
|
189
|
+
"""
|
|
190
|
+
camel_field = snake_to_camel(input_str=field)
|
|
191
|
+
_, interfaces_with_optional_properties = self._find_matching_interfaces(
|
|
192
|
+
model_name
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return any(
|
|
196
|
+
camel_field in properties
|
|
197
|
+
for properties in interfaces_with_optional_properties.values()
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _ts_interface_properties_ordered(
|
|
201
|
+
self, model_name: str, fields: list[str]
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Check if the order of the TypeScript interface properties exactly matches that of the backend model fields.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
model_name : str
|
|
209
|
+
The name of the model to check the frontend TypeScript file for.
|
|
210
|
+
|
|
211
|
+
fields : List[str]
|
|
212
|
+
The fields of the backend model.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
bool
|
|
217
|
+
Whether the order of the properties of the TypeScript interface file match that of the backend model fields.
|
|
218
|
+
"""
|
|
219
|
+
camel_fields = [snake_to_camel(input_str=f) for f in fields]
|
|
220
|
+
interfaces, _ = self._find_matching_interfaces(model_name)
|
|
221
|
+
|
|
222
|
+
return all(
|
|
223
|
+
is_ordered_subset(
|
|
224
|
+
reference_list=camel_fields, candidate_sub_list=interfaces[i]
|
|
225
|
+
)
|
|
226
|
+
for i in interfaces
|
|
227
|
+
)
|
|
228
|
+
|
|
135
229
|
@staticmethod
|
|
136
230
|
def _format_missing_interface_message(model_name: str) -> str:
|
|
137
231
|
"""
|
|
@@ -147,15 +241,17 @@ class TypeChecker:
|
|
|
147
241
|
str
|
|
148
242
|
The message displayed to the user when missing interfaces are found.
|
|
149
243
|
"""
|
|
150
|
-
potential_names = TypeChecker._generate_potential_names(model_name)
|
|
151
244
|
return (
|
|
152
|
-
f"\nNo matching TypeScript interface found for model
|
|
153
|
-
|
|
245
|
+
f"\nNo matching TypeScript interface found for the model '{model_name}'."
|
|
246
|
+
"\nPlease name your TypeScript interfaces the same as the corresponding backend models."
|
|
247
|
+
"\nYou can also use the 'backend_to_ts_model_name_conversions' option within the configuration file."
|
|
248
|
+
"\nThe key is the backend model name and the value is a list of the corresponding interfaces."
|
|
249
|
+
"\nThis option is also how you can break larger backend models into multiple interfaces that extend one another."
|
|
154
250
|
)
|
|
155
251
|
|
|
156
252
|
@staticmethod
|
|
157
253
|
def _format_missing_field_message(
|
|
158
|
-
field: str, model_name: str, interfaces: Dict[str,
|
|
254
|
+
field: str, model_name: str, interfaces: Dict[str, List[str]]
|
|
159
255
|
) -> str:
|
|
160
256
|
"""
|
|
161
257
|
Format message for missing field.
|
|
@@ -184,5 +280,64 @@ class TypeChecker:
|
|
|
184
280
|
return (
|
|
185
281
|
f"\nField '{field}' (camelCase: '{camel_field}') from model '{model_name}' is missing in the TypeScript interfaces."
|
|
186
282
|
f"\nExpected to find this field in the frontend {interface_of_interfaces}: {', '.join(interfaces.keys())}"
|
|
187
|
-
f"\nTo ignore this field, add the following comment to the TypeScript file: '// ts-backend-check: ignore field {camel_field}'"
|
|
283
|
+
f"\nTo ignore this field, add the following comment to the TypeScript file (in order based on the model fields): '// ts-backend-check: ignore field {camel_field}'"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _format_optional_properties_message(
|
|
288
|
+
field: str, model_name: str, models_file: str, types_file: str
|
|
289
|
+
) -> str:
|
|
290
|
+
"""
|
|
291
|
+
Format message for when the blank status of a model field doesn't match the optional status of the corresponding property.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
field : str
|
|
296
|
+
The model field that doesn't match blank and optional states.
|
|
297
|
+
|
|
298
|
+
model_name : str
|
|
299
|
+
The name of the model that the mismatch occurs in.
|
|
300
|
+
|
|
301
|
+
models_file : str
|
|
302
|
+
The file path for the models file to check.
|
|
303
|
+
|
|
304
|
+
types_file : str
|
|
305
|
+
The file path for the TypeScript interfaces file that was checked.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
str
|
|
310
|
+
The message displayed to the user when missing fields are found.
|
|
311
|
+
"""
|
|
312
|
+
camel_field = snake_to_camel(input_str=field)
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
f"\nField '{field}' (camelCase: '{camel_field}') from model '{model_name}' doesn't match the TypeScript interfaces based on blank to optional agreement."
|
|
316
|
+
f"\nPlease check '{models_file}' and '{types_file}' to make sure that all 'blank=True' fields are optional (?) in the TypeScript interfaces file."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _format_unordered_interface_properties_message(
|
|
321
|
+
models_file: str, types_file: str
|
|
322
|
+
) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Format message for unordered interface properties.
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
models_file : str
|
|
329
|
+
The file path for the models file to check.
|
|
330
|
+
|
|
331
|
+
types_file : str
|
|
332
|
+
The file path for the TypeScript interfaces file that was checked.
|
|
333
|
+
|
|
334
|
+
Returns
|
|
335
|
+
-------
|
|
336
|
+
str
|
|
337
|
+
The message displayed to the user when unordered interface properties are found.
|
|
338
|
+
"""
|
|
339
|
+
return (
|
|
340
|
+
f"\nThe properties of the interface file '{types_file}' are unordered."
|
|
341
|
+
f"\nAll interface properties should exactly match the order of the corresponding fields in the '{models_file}' backend model."
|
|
342
|
+
"\nIf the model is synced with multiple interfaces, then their properties should follow the order prescribed by the model fields."
|
|
188
343
|
)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""
|
|
3
|
+
Configure cli to run based on a YAML configuration file.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich import print as rprint
|
|
9
|
+
from yaml import dump
|
|
10
|
+
|
|
11
|
+
YAML_CONFIG_FILE_PATH = (
|
|
12
|
+
Path(__file__).parent.parent.parent.parent / ".ts-backend-check.yaml"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
PROJECT_ROOT_PATH = Path(__file__).parent.parent.parent.parent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def path_exists(path: str) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Check if path entered by the user exists withing the filesystem.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
path : str
|
|
25
|
+
Path should be entered as a string from the user.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
bool
|
|
30
|
+
Return true or false based on if path exists.
|
|
31
|
+
"""
|
|
32
|
+
full_path = Path(__file__).parent.parent.parent.parent / path
|
|
33
|
+
return bool(Path(full_path).is_file())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def write_config(config: dict[str, dict[str, object]]) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Function to write into .ts-backend-check.yaml file.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
config : dict[str, dict[str, object]]
|
|
43
|
+
Passing a dictionary as key str with another dict as value.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
options = f"""# Configuration file for ts-backend-check validation.
|
|
47
|
+
# See https://github.com/activist-org/ts-backend-check for details.
|
|
48
|
+
|
|
49
|
+
{dump(config, sort_keys=False)}"""
|
|
50
|
+
|
|
51
|
+
with open(YAML_CONFIG_FILE_PATH, "w") as file:
|
|
52
|
+
file.write(options)
|
|
53
|
+
|
|
54
|
+
except IOError as e:
|
|
55
|
+
print(f"Error while writing configuration file: {e}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def configure_model_interface_arguments() -> None:
|
|
59
|
+
"""
|
|
60
|
+
Function to receive paths from user.
|
|
61
|
+
"""
|
|
62
|
+
config_options = {}
|
|
63
|
+
while True:
|
|
64
|
+
print(
|
|
65
|
+
"\nAdding new model-interface configuration. Please provide the information as directed:"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
key = input(
|
|
69
|
+
"Enter a model-interface identifier (eg: auth, user, event): "
|
|
70
|
+
).strip()
|
|
71
|
+
if not key:
|
|
72
|
+
rprint(
|
|
73
|
+
"\n[red]The model-interface identifier cannot be empty. Please try again.[/red]"
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# Get backend path.
|
|
78
|
+
while True:
|
|
79
|
+
backend_path = input("Enter the path for Django models.py file: ").strip()
|
|
80
|
+
if not backend_path:
|
|
81
|
+
rprint(
|
|
82
|
+
"[red]The path for the Django models.py file cannot be empty. Please try again.[/red]"
|
|
83
|
+
)
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if path_exists(backend_path):
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
rprint(f"[red]File not found: {PROJECT_ROOT_PATH / backend_path}[/red]")
|
|
90
|
+
rprint("[yellow]Please check the path and try again.[/yellow]")
|
|
91
|
+
|
|
92
|
+
# Get frontend path.
|
|
93
|
+
while True:
|
|
94
|
+
frontend_path = input(
|
|
95
|
+
"Enter the path to TypeScript interface file: "
|
|
96
|
+
).strip()
|
|
97
|
+
if not frontend_path:
|
|
98
|
+
rprint(
|
|
99
|
+
"[red]The path for the TypeScript interface file cannot be empty. Please try again.[/red]"
|
|
100
|
+
)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if path_exists(frontend_path):
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
rprint(f"[red]File not found: {PROJECT_ROOT_PATH / frontend_path}[/red]")
|
|
107
|
+
rprint("[yellow]Please check the path and try again.[/yellow]")
|
|
108
|
+
|
|
109
|
+
# Get whether to check blank model fields.
|
|
110
|
+
while True:
|
|
111
|
+
check_blank_model_fields_response = (
|
|
112
|
+
input(
|
|
113
|
+
"The check should assert that model fields that can be blank must also be optional in interfaces ([y]/n): "
|
|
114
|
+
)
|
|
115
|
+
.strip()
|
|
116
|
+
.lower()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if check_blank_model_fields_response in ["y", ""]:
|
|
120
|
+
check_blank_model_fields = True
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
elif check_blank_model_fields_response == "n":
|
|
124
|
+
check_blank_model_fields = False
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
else:
|
|
128
|
+
rprint("[red]Invalid response. Please try again.[/red]")
|
|
129
|
+
|
|
130
|
+
# Get model name conversions.
|
|
131
|
+
rprint(
|
|
132
|
+
"[yellow]💡 Note: You need model name conversions if your TypeScript interfaces are not named exactly the same as the corresponding models (i.e. UserModel in Django and User in TS).[/yellow]"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
backend_to_ts_model_name_conversions = {}
|
|
136
|
+
while True:
|
|
137
|
+
name_conversions_needed = (
|
|
138
|
+
input("Model name conversions are needed (y/[n]): ").strip().lower()
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if name_conversions_needed in ["n", ""]:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
while True:
|
|
145
|
+
while True:
|
|
146
|
+
if backend_model_name := input(
|
|
147
|
+
"Enter the backend model name: "
|
|
148
|
+
).strip():
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
else:
|
|
152
|
+
rprint("[red]Invalid response. Please try again.[/red]")
|
|
153
|
+
|
|
154
|
+
while True:
|
|
155
|
+
if ts_interface_name := input(
|
|
156
|
+
"Enter the TypeScript interface name: "
|
|
157
|
+
).strip():
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
rprint("[red]Invalid response. Please try again.[/red]")
|
|
162
|
+
|
|
163
|
+
backend_to_ts_model_name_conversions[backend_model_name] = (
|
|
164
|
+
ts_interface_name
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
further_name_conversions_needed = (
|
|
168
|
+
input("Add more model name conversions (y/[n]): ").strip().lower()
|
|
169
|
+
)
|
|
170
|
+
if further_name_conversions_needed in ["n", ""]:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
config_options[key] = {
|
|
176
|
+
"backend_model_path": backend_path,
|
|
177
|
+
"ts_interface_path": frontend_path,
|
|
178
|
+
"check_blank_model_fields": check_blank_model_fields,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if backend_to_ts_model_name_conversions:
|
|
182
|
+
config_options[key]["backend_to_ts_model_name_conversions"] = (
|
|
183
|
+
backend_to_ts_model_name_conversions
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
write_config(config_options)
|
|
187
|
+
rprint(f"[green]✅ Added configuration for '{key}' check.[/green]")
|
|
188
|
+
|
|
189
|
+
continue_config = input(
|
|
190
|
+
"Add another model-interface configuration (y/[n]): "
|
|
191
|
+
).strip()
|
|
192
|
+
|
|
193
|
+
if continue_config.lower() in ["n", ""]:
|
|
194
|
+
if config_options:
|
|
195
|
+
optional_s = "s" if len(config_options) > 1 else ""
|
|
196
|
+
rprint(
|
|
197
|
+
f"[green]✅ Configuration complete! Added {len(config_options)} configuration{optional_s} to check.[/green]"
|
|
198
|
+
)
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def generate_config_file() -> None:
|
|
203
|
+
"""
|
|
204
|
+
Main function to create or update configuration.
|
|
205
|
+
"""
|
|
206
|
+
header = "ts-backend-check Configuration Setup"
|
|
207
|
+
print(header)
|
|
208
|
+
print("=" * len(header))
|
|
209
|
+
|
|
210
|
+
if YAML_CONFIG_FILE_PATH.is_file():
|
|
211
|
+
reconfigure_choice = input(
|
|
212
|
+
"Configuration file exists. Confirm if you want to re-configure your .ts-backend-check.yaml file (y/[n]): "
|
|
213
|
+
)
|
|
214
|
+
if reconfigure_choice.lower() in ["n", ""]:
|
|
215
|
+
print("Exiting without changes.")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
print("Reconfiguring...")
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
print("Creating new configuration file...")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
configure_model_interface_arguments()
|
|
225
|
+
|
|
226
|
+
except KeyboardInterrupt:
|
|
227
|
+
print("\n\nConfiguration cancelled by user.")
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(f"\nError during configuration: {e}")
|
|
231
|
+
print("Configuration cancelled.")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
generate_config_file()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""
|
|
3
|
+
Functionality to copy the test project files from the package to the present working directory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Check for Windows and derive directory path separator.
|
|
11
|
+
PATH_SEPARATOR = "\\" if os.name == "nt" else "/"
|
|
12
|
+
INTERNAL_TEST_PROJECT_DIR_PATH = Path(__file__).parent.parent / "test_project"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_test_project() -> None:
|
|
16
|
+
"""
|
|
17
|
+
Copy the ts_backend_check/test_project directory to the present working directory.
|
|
18
|
+
"""
|
|
19
|
+
if not Path("./ts_backend_check_test_project/").is_dir():
|
|
20
|
+
print(
|
|
21
|
+
f"Generating testing project for ts-backend-check in .{PATH_SEPARATOR}ts_backend_check_test_project{PATH_SEPARATOR} ..."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
shutil.copytree(
|
|
25
|
+
INTERNAL_TEST_PROJECT_DIR_PATH,
|
|
26
|
+
Path("./ts_backend_check_test_project/"),
|
|
27
|
+
dirs_exist_ok=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
print("The test project has been successfully generated.")
|
|
31
|
+
print(
|
|
32
|
+
"Within the project there's one model that passes all checks and one that fails all checks."
|
|
33
|
+
)
|
|
34
|
+
if (
|
|
35
|
+
not Path(".ts-backend-check.yaml").is_file()
|
|
36
|
+
and not Path(".ts-backend-check.yml").is_file()
|
|
37
|
+
):
|
|
38
|
+
print(
|
|
39
|
+
"You can set which one to test in an ts-backend-check configuration file."
|
|
40
|
+
)
|
|
41
|
+
print(
|
|
42
|
+
"Please generate one with the 'ts-backend-check --generate-config-file' command."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
elif Path(".ts-backend-check.yaml").is_file():
|
|
46
|
+
print("You can set which one to test in the .ts-backend-check.yaml file.")
|
|
47
|
+
|
|
48
|
+
elif Path(".ts-backend-check.yml").is_file():
|
|
49
|
+
print("You can set which one to test in the .ts-backend-check.yml file.")
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
print(
|
|
53
|
+
f"Test project for ts-backend-check already exist in .{PATH_SEPARATOR}ts_backend_check_test_project{PATH_SEPARATOR} and will not be regenerated."
|
|
54
|
+
)
|