ts-backend-check 1.2.1__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.1.dist-info → ts_backend_check-1.3.0.dist-info}/METADATA +175 -57
- ts_backend_check-1.3.0.dist-info/RECORD +21 -0
- {ts_backend_check-1.2.1.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.1.data/data/requirements.txt +0 -26
- ts_backend_check-1.2.1.dist-info/RECORD +0 -21
- {ts_backend_check-1.2.1.dist-info → ts_backend_check-1.3.0.dist-info}/entry_points.txt +0 -0
- {ts_backend_check-1.2.1.dist-info → ts_backend_check-1.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {ts_backend_check-1.2.1.dist-info → ts_backend_check-1.3.0.dist-info}/top_level.txt +0 -0
ts_backend_check/cli/main.py
CHANGED
|
@@ -8,16 +8,137 @@ import sys
|
|
|
8
8
|
from argparse import ArgumentParser
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
import yaml
|
|
11
12
|
from rich import print as rprint
|
|
12
13
|
from rich.text import Text
|
|
13
14
|
|
|
14
15
|
from ts_backend_check.checker import TypeChecker
|
|
15
|
-
from ts_backend_check.cli.
|
|
16
|
-
from ts_backend_check.cli.
|
|
16
|
+
from ts_backend_check.cli.generate_config_file import generate_config_file
|
|
17
|
+
from ts_backend_check.cli.generate_test_project import generate_test_project
|
|
17
18
|
from ts_backend_check.cli.upgrade import upgrade_cli
|
|
18
19
|
from ts_backend_check.cli.version import get_version_message
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
# MARK: Base Paths
|
|
22
|
+
|
|
23
|
+
CWD_PATH = Path.cwd()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_config_file_path() -> Path:
|
|
27
|
+
"""
|
|
28
|
+
Get the path to the ts-backend-check configuration file.
|
|
29
|
+
|
|
30
|
+
Checks for both .yaml and .yml extensions, preferring .yaml if both exist.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
Path
|
|
35
|
+
The path to the configuration file (.yaml or .yml).
|
|
36
|
+
"""
|
|
37
|
+
yaml_path = CWD_PATH / ".ts-backend-check.yaml"
|
|
38
|
+
yml_path = CWD_PATH / ".ts-backend-check.yml"
|
|
39
|
+
|
|
40
|
+
# Prefer .yaml if it exists, otherwise check for .yml.
|
|
41
|
+
if yaml_path.is_file():
|
|
42
|
+
return yaml_path
|
|
43
|
+
|
|
44
|
+
elif yml_path.is_file():
|
|
45
|
+
return yml_path
|
|
46
|
+
|
|
47
|
+
else:
|
|
48
|
+
# Default to .yaml for new files.
|
|
49
|
+
return yaml_path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
YAML_CONFIG_FILE_PATH = get_config_file_path()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# MARK: CLI Vars
|
|
56
|
+
|
|
57
|
+
if not Path(YAML_CONFIG_FILE_PATH).is_file():
|
|
58
|
+
generate_config_file()
|
|
59
|
+
|
|
60
|
+
if not Path(YAML_CONFIG_FILE_PATH).is_file():
|
|
61
|
+
print(
|
|
62
|
+
"No configuration file. Please generate a configuration file (.ts-backend-check.yaml or .ts-backend-check.yml) with ts-backend-check -gcf."
|
|
63
|
+
)
|
|
64
|
+
exit(1)
|
|
65
|
+
|
|
66
|
+
with open(YAML_CONFIG_FILE_PATH, "r", encoding="utf-8") as file:
|
|
67
|
+
config = yaml.safe_load(file)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_files_and_print_results(
|
|
71
|
+
identifier: str,
|
|
72
|
+
backend_model_file_path: Path,
|
|
73
|
+
ts_interface_file_path: Path,
|
|
74
|
+
check_blank: bool = False,
|
|
75
|
+
model_name_conversions: dict[str, list[str]] = {},
|
|
76
|
+
) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Check the provided files for the given model and print the results.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
identifier : str
|
|
83
|
+
The model in the .ts-backend-check.yaml configuration file to check models and interfaces for.
|
|
84
|
+
|
|
85
|
+
backend_model_file_path : Path
|
|
86
|
+
The path to the backend models as defined in the .ts-backend-check.yaml configuration file.
|
|
87
|
+
|
|
88
|
+
ts_interface_file_path : Path
|
|
89
|
+
The path to the TypeScript interfaces as defined in the .ts-backend-check.yaml configuration file.
|
|
90
|
+
|
|
91
|
+
check_blank : bool, default=False
|
|
92
|
+
Whether to also check that fields marked 'blank=True' within Django models are optional (?) in the TypeScript interfaces.
|
|
93
|
+
|
|
94
|
+
model_name_conversions : dict[str, list[str]], default={}
|
|
95
|
+
A dictionary of backend model names to their corresponding TypeScript interfaces when snake to camel case isn't valid.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
bool
|
|
100
|
+
Whether the checks passed (True) or not (False).
|
|
101
|
+
"""
|
|
102
|
+
if not backend_model_file_path.is_file():
|
|
103
|
+
rprint(
|
|
104
|
+
f"[red]❌ {backend_model_file_path} that should contain the '{identifier}' backend models does not exist. Please check the ts-backend-check configuration file and try again.[/red]"
|
|
105
|
+
)
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
elif not ts_interface_file_path.is_file():
|
|
109
|
+
rprint(
|
|
110
|
+
f"[red]❌ {ts_interface_file_path} that should contain the '{identifier}' TypeScript types does not exist. Please check the ts-backend-check configuration file and try again.[/red]"
|
|
111
|
+
)
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
checker = TypeChecker(
|
|
115
|
+
models_file=str(backend_model_file_path),
|
|
116
|
+
types_file=str(ts_interface_file_path),
|
|
117
|
+
model_name_conversions=model_name_conversions,
|
|
118
|
+
check_blank=check_blank,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if missing := checker.check():
|
|
122
|
+
rprint(
|
|
123
|
+
f"\n[red]❌ ts-backend-check error: There are inconsistencies between the provided '{identifier}' backend models and TypeScript interfaces. Please see the output below for details.[/red]"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
for msg in missing:
|
|
127
|
+
rprint(Text.from_markup(f"[red]{msg}[/red]"))
|
|
128
|
+
|
|
129
|
+
error_or_errors = "errors" if len(missing) > 1 else "error"
|
|
130
|
+
rprint(
|
|
131
|
+
f"[red]\nPlease fix the {len(missing)} {error_or_errors} above to continue the sync of the backend models of {backend_model_file_path} and the TypeScript interfaces of {ts_interface_file_path}.[/red]"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
else:
|
|
137
|
+
rprint(
|
|
138
|
+
f"[green]✅ Success: All backend models are synced with their corresponding TypeScript interfaces for the provided '{identifier}' files.[/green]"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return True
|
|
21
142
|
|
|
22
143
|
|
|
23
144
|
def main() -> None:
|
|
@@ -27,12 +148,19 @@ def main() -> None:
|
|
|
27
148
|
Notes
|
|
28
149
|
-----
|
|
29
150
|
The available command line arguments are:
|
|
30
|
-
- --
|
|
31
|
-
- --
|
|
151
|
+
- --help (-h): Show this help message and exit.
|
|
152
|
+
- --version (-v): Show the version of the ts-backend-check CLI.
|
|
153
|
+
- --upgrade (-u): Upgrade the ts-backend-check CLI to the latest version.
|
|
154
|
+
- --generate-config-file (-gcf): Interactively generate a configuration file for ts-backend-check.
|
|
155
|
+
- --generate-test-project (-gtp): Generate project to test ts-backend-check functionalities.
|
|
156
|
+
- --model (-m): The model in the .ts-backend-check.yaml configuration file to check.
|
|
157
|
+
- --all (-a): Run checks of all backend models against their corresponding TypeScript interfaces.
|
|
32
158
|
|
|
33
159
|
Examples
|
|
34
160
|
--------
|
|
35
|
-
>>> ts-backend-check -
|
|
161
|
+
>>> ts-backend-check --generate-config-file # -gcf
|
|
162
|
+
>>> ts-backend-check --model <ts-backend-check-config-file-model> # -m
|
|
163
|
+
>>> ts-backend-check --all # -a
|
|
36
164
|
"""
|
|
37
165
|
# MARK: CLI Base
|
|
38
166
|
|
|
@@ -61,28 +189,30 @@ def main() -> None:
|
|
|
61
189
|
)
|
|
62
190
|
|
|
63
191
|
parser.add_argument(
|
|
64
|
-
"-
|
|
65
|
-
"--
|
|
66
|
-
|
|
192
|
+
"-gcf",
|
|
193
|
+
"--generate-config-file",
|
|
194
|
+
action="store_true",
|
|
195
|
+
help="Interactively generate a configuration file for ts-backend-check.",
|
|
67
196
|
)
|
|
68
197
|
|
|
69
198
|
parser.add_argument(
|
|
70
|
-
"-
|
|
71
|
-
"--
|
|
72
|
-
|
|
199
|
+
"-gtp",
|
|
200
|
+
"--generate-test-project",
|
|
201
|
+
action="store_true",
|
|
202
|
+
help="Generate project to test ts-backend-check functionalities.",
|
|
73
203
|
)
|
|
74
204
|
|
|
75
205
|
parser.add_argument(
|
|
76
|
-
"-
|
|
77
|
-
"--
|
|
78
|
-
|
|
79
|
-
help="Configure a YAML file to simplify your checks.",
|
|
206
|
+
"-m",
|
|
207
|
+
"--model",
|
|
208
|
+
help="The model in the .ts-backend-check.yaml configuration file to check.",
|
|
80
209
|
)
|
|
81
210
|
|
|
82
211
|
parser.add_argument(
|
|
83
|
-
"-
|
|
84
|
-
"--
|
|
85
|
-
|
|
212
|
+
"-a",
|
|
213
|
+
"--all",
|
|
214
|
+
action="store_true",
|
|
215
|
+
help="Run checks of all backend models against their corresponding TypeScript interfaces.",
|
|
86
216
|
)
|
|
87
217
|
|
|
88
218
|
# MARK: Setup CLI
|
|
@@ -93,53 +223,84 @@ def main() -> None:
|
|
|
93
223
|
upgrade_cli()
|
|
94
224
|
return
|
|
95
225
|
|
|
96
|
-
if args.
|
|
97
|
-
|
|
226
|
+
if args.generate_config_file:
|
|
227
|
+
generate_config_file()
|
|
98
228
|
return
|
|
99
229
|
|
|
100
|
-
if args.
|
|
101
|
-
|
|
230
|
+
if args.generate_test_project:
|
|
231
|
+
generate_test_project()
|
|
102
232
|
return
|
|
103
233
|
|
|
104
|
-
# MARK: Run
|
|
234
|
+
# MARK: Run Checks
|
|
105
235
|
|
|
106
|
-
|
|
107
|
-
ts_file_path = ROOT_DIR / args.typescript_file
|
|
236
|
+
results = []
|
|
108
237
|
|
|
109
|
-
if
|
|
110
|
-
|
|
111
|
-
f"[red]{args.backend_model_file} that should contain the backend models does not exist. Please check and try again.[/red]"
|
|
112
|
-
)
|
|
238
|
+
if args.model:
|
|
239
|
+
model_config = config.get(args.model)
|
|
113
240
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
241
|
+
if not model_config:
|
|
242
|
+
rprint(
|
|
243
|
+
f"[red]{args.model} is not an index within the .ts-backend-check.yaml configuration file. Please check the defined models and try again.[/red]"
|
|
244
|
+
)
|
|
245
|
+
sys.exit(1)
|
|
118
246
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
247
|
+
config_backend_model_file_path = Path(model_config["backend_model_path"])
|
|
248
|
+
config_ts_interface_file_path = Path(model_config["ts_interface_path"])
|
|
249
|
+
config_check_blank = (
|
|
250
|
+
model_config["check_blank_model_fields"]
|
|
251
|
+
if "check_blank_model_fields" in model_config
|
|
252
|
+
else False
|
|
253
|
+
)
|
|
254
|
+
config_model_name_conversions = (
|
|
255
|
+
model_config["backend_to_ts_model_name_conversions"]
|
|
256
|
+
if "backend_to_ts_model_name_conversions" in model_config
|
|
257
|
+
else {}
|
|
123
258
|
)
|
|
124
259
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
260
|
+
r = check_files_and_print_results(
|
|
261
|
+
identifier=args.model,
|
|
262
|
+
backend_model_file_path=config_backend_model_file_path,
|
|
263
|
+
ts_interface_file_path=config_ts_interface_file_path,
|
|
264
|
+
check_blank=config_check_blank,
|
|
265
|
+
model_name_conversions=config_model_name_conversions,
|
|
266
|
+
)
|
|
267
|
+
results.append(r)
|
|
268
|
+
|
|
269
|
+
if args.all:
|
|
270
|
+
for i in config.keys():
|
|
271
|
+
model_config = config.get(i)
|
|
272
|
+
|
|
273
|
+
config_backend_model_file_path = Path(model_config["backend_model_path"])
|
|
274
|
+
config_ts_interface_file_path = Path(model_config["ts_interface_path"])
|
|
275
|
+
config_check_blank = (
|
|
276
|
+
model_config["check_blank_model_fields"]
|
|
277
|
+
if "check_blank_model_fields" in model_config
|
|
278
|
+
else False
|
|
279
|
+
)
|
|
280
|
+
config_model_name_conversions = (
|
|
281
|
+
model_config["backend_to_ts_model_name_conversions"]
|
|
282
|
+
if "backend_to_ts_model_name_conversions" in model_config
|
|
283
|
+
else {}
|
|
128
284
|
)
|
|
129
285
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
f"[red]Please fix the {len(missing)} {field_or_fields} above to have the backend models of {args.backend_model_file} synced with the typescript interfaces of {(args.typescript_file)}.[/red]"
|
|
286
|
+
r = check_files_and_print_results(
|
|
287
|
+
identifier=i,
|
|
288
|
+
backend_model_file_path=config_backend_model_file_path,
|
|
289
|
+
ts_interface_file_path=config_ts_interface_file_path,
|
|
290
|
+
check_blank=config_check_blank,
|
|
291
|
+
model_name_conversions=config_model_name_conversions,
|
|
137
292
|
)
|
|
293
|
+
results.append(r)
|
|
294
|
+
|
|
295
|
+
if args.model or args.all:
|
|
296
|
+
if not all(results):
|
|
138
297
|
sys.exit(1)
|
|
139
298
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
299
|
+
else:
|
|
300
|
+
return # exit 0
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
parser.print_help()
|
|
143
304
|
|
|
144
305
|
|
|
145
306
|
if __name__ == "__main__":
|
|
@@ -4,7 +4,7 @@ Module for parsing Django models and extracting field information.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import ast
|
|
7
|
-
from typing import Dict,
|
|
7
|
+
from typing import Dict, List, Tuple
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class DjangoModelVisitor(ast.NodeVisitor):
|
|
@@ -31,8 +31,9 @@ class DjangoModelVisitor(ast.NodeVisitor):
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
def __init__(self) -> None:
|
|
34
|
-
self.models: Dict[str,
|
|
34
|
+
self.models: Dict[str, List[str]] = {}
|
|
35
35
|
self.current_model: str | None = None
|
|
36
|
+
self.models_and_blank_fields: Dict[str, List[str]] = {}
|
|
36
37
|
|
|
37
38
|
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
38
39
|
"""
|
|
@@ -48,7 +49,7 @@ class DjangoModelVisitor(ast.NodeVisitor):
|
|
|
48
49
|
if node.bases:
|
|
49
50
|
self.current_model = node.name
|
|
50
51
|
if self.current_model not in self.models:
|
|
51
|
-
self.models[self.current_model] =
|
|
52
|
+
self.models[self.current_model] = []
|
|
52
53
|
|
|
53
54
|
self.generic_visit(node)
|
|
54
55
|
|
|
@@ -77,10 +78,23 @@ class DjangoModelVisitor(ast.NodeVisitor):
|
|
|
77
78
|
field_type in node.value.func.attr
|
|
78
79
|
for field_type in self.DJANGO_FIELD_TYPES
|
|
79
80
|
):
|
|
80
|
-
self.models[self.current_model].
|
|
81
|
+
self.models[self.current_model].append(target.id)
|
|
81
82
|
|
|
83
|
+
if any(
|
|
84
|
+
kw.arg == "blank"
|
|
85
|
+
and isinstance(kw.value, ast.Constant)
|
|
86
|
+
and kw.value.value is True
|
|
87
|
+
for kw in node.value.keywords
|
|
88
|
+
):
|
|
89
|
+
if self.current_model not in self.models_and_blank_fields:
|
|
90
|
+
self.models_and_blank_fields[self.current_model] = []
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
self.models_and_blank_fields[self.current_model].append(target.id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_model_fields(
|
|
96
|
+
models_file: str,
|
|
97
|
+
) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:
|
|
84
98
|
"""
|
|
85
99
|
Extract fields from Django models file.
|
|
86
100
|
|
|
@@ -91,7 +105,7 @@ def extract_model_fields(models_file: str) -> Dict[str, Set[str]]:
|
|
|
91
105
|
|
|
92
106
|
Returns
|
|
93
107
|
-------
|
|
94
|
-
Dict[str,
|
|
108
|
+
Tuple(Dict[str, List[str]], Dict[str, List[str]])
|
|
95
109
|
The fields from the models file extracted into a dictionary for future processing.
|
|
96
110
|
"""
|
|
97
111
|
with open(models_file, "r", encoding="utf-8") as f:
|
|
@@ -102,6 +116,7 @@ def extract_model_fields(models_file: str) -> Dict[str, Set[str]]:
|
|
|
102
116
|
|
|
103
117
|
try:
|
|
104
118
|
tree = ast.parse(content)
|
|
119
|
+
|
|
105
120
|
except SyntaxError as e:
|
|
106
121
|
raise SyntaxError(
|
|
107
122
|
f"Failed to parse {models_file}. Make sure it's a valid Python file. Error: {str(e)}"
|
|
@@ -110,4 +125,4 @@ def extract_model_fields(models_file: str) -> Dict[str, Set[str]]:
|
|
|
110
125
|
visitor = DjangoModelVisitor()
|
|
111
126
|
visitor.visit(tree)
|
|
112
127
|
|
|
113
|
-
return visitor.models
|
|
128
|
+
return visitor.models, visitor.models_and_blank_fields
|
|
@@ -11,11 +11,12 @@ from typing import Dict, List, Set
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class TypeScriptInterface:
|
|
13
13
|
"""
|
|
14
|
-
Represents a TypeScript interface with its
|
|
14
|
+
Represents a TypeScript interface with its properties and parent interfaces.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
name: str
|
|
18
|
-
|
|
18
|
+
properties: List[str]
|
|
19
|
+
optional_properties: List[str]
|
|
19
20
|
parents: List[str]
|
|
20
21
|
|
|
21
22
|
|
|
@@ -54,9 +55,12 @@ class TypeScriptParser:
|
|
|
54
55
|
parents = (
|
|
55
56
|
[p.strip() for p in match.group(2).split(",")] if match.group(2) else []
|
|
56
57
|
)
|
|
57
|
-
|
|
58
|
+
properties = self._extract_properties(match.group(3))
|
|
59
|
+
optional_properties = self._extract_optional_properties(match.group(3))
|
|
58
60
|
|
|
59
|
-
interfaces[name] = TypeScriptInterface(
|
|
61
|
+
interfaces[name] = TypeScriptInterface(
|
|
62
|
+
name, properties, optional_properties, parents
|
|
63
|
+
)
|
|
60
64
|
|
|
61
65
|
return interfaces
|
|
62
66
|
|
|
@@ -73,9 +77,9 @@ class TypeScriptParser:
|
|
|
73
77
|
return set(re.findall(ignore_pattern, self.content))
|
|
74
78
|
|
|
75
79
|
@staticmethod
|
|
76
|
-
def
|
|
80
|
+
def _extract_properties(interface_body: str) -> List[str]:
|
|
77
81
|
"""
|
|
78
|
-
Extract
|
|
82
|
+
Extract both real properties and 'ignored' comment backend fields from interface bodies.
|
|
79
83
|
|
|
80
84
|
Parameters
|
|
81
85
|
----------
|
|
@@ -84,15 +88,43 @@ class TypeScriptParser:
|
|
|
84
88
|
|
|
85
89
|
Returns
|
|
86
90
|
-------
|
|
87
|
-
|
|
88
|
-
The
|
|
91
|
+
List[str]
|
|
92
|
+
The property names from the model interface body.
|
|
93
|
+
"""
|
|
94
|
+
combined_pattern = (
|
|
95
|
+
r"^\s*(?:"
|
|
96
|
+
r"(?:readonly\s+)?(\w+)\s*\??\s*:|" # standard/readonly properties
|
|
97
|
+
r"//\s*ts-backend-check:\s*ignore\s+field\s+(\w+)" # ts-backend-check ignore comment properties
|
|
98
|
+
r")"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
properties = []
|
|
102
|
+
for match in re.finditer(combined_pattern, interface_body, flags=re.MULTILINE):
|
|
103
|
+
if field_name := match.group(1) or match.group(2):
|
|
104
|
+
properties.append(field_name)
|
|
105
|
+
|
|
106
|
+
return properties
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _extract_optional_properties(interface_body: str) -> List[str]:
|
|
89
110
|
"""
|
|
90
|
-
|
|
111
|
+
Extract all optional properties from interface bodies.
|
|
91
112
|
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
interface_body : str
|
|
116
|
+
A string representation of the interface body of the model.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
List[str]
|
|
121
|
+
The optional property names from the model interface body.
|
|
122
|
+
"""
|
|
123
|
+
pattern = r"^\s*(?:readonly\s+)?(\w+)\s*\?:" # optional properties
|
|
94
124
|
|
|
95
|
-
|
|
96
|
-
|
|
125
|
+
optional_properties = []
|
|
126
|
+
for match in re.finditer(pattern, interface_body, flags=re.MULTILINE):
|
|
127
|
+
if field_name := match.group(1):
|
|
128
|
+
optional_properties.append(field_name)
|
|
97
129
|
|
|
98
|
-
return
|
|
130
|
+
return optional_properties
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""
|
|
3
|
+
Example backend model file that has both valid and invalid TS interface files in test_project/frontend.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from django.db import models
|
|
7
|
+
|
|
8
|
+
# mypy: ignore-errors
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventModel(models.Model):
|
|
12
|
+
"""
|
|
13
|
+
Model for events that has both valid and invalid corresponding TS interface files.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
title = models.CharField(max_length=200)
|
|
17
|
+
description = models.TextField()
|
|
18
|
+
organizer = models.ForeignKey("User", on_delete=models.CASCADE)
|
|
19
|
+
participants = models.ManyToManyField("User", related_name="events", blank=True)
|
|
20
|
+
is_private = models.BooleanField(default=True)
|
|
21
|
+
date = models.DateTimeField()
|
|
22
|
+
_private_field = models.CharField(max_length=100) # should be ignored
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UserModel(models.Model):
|
|
26
|
+
"""
|
|
27
|
+
Model for users that has both valid and invalid corresponding TS interface files.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
id = models.CharField(max_length=32)
|
|
31
|
+
name = models.CharField(max_length=50)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Attn: EventModel is missing a field from backend/models.py.
|
|
3
|
+
export interface Event {
|
|
4
|
+
// Note: EventModel is mapped to Event and EventExtended via backend_to_ts_model_name_conversions.
|
|
5
|
+
title: string;
|
|
6
|
+
organizer: User;
|
|
7
|
+
// Attn: participants is not optional.
|
|
8
|
+
participants: User[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EventExtended extends Event {
|
|
12
|
+
// Attn: date is out of order below, but this won't be reported as we have missing fields.
|
|
13
|
+
// ts-backend-check: ignore field date
|
|
14
|
+
isPrivate: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Attn: User is not synced to UserModel via backend_to_ts_model_name_conversions.
|
|
18
|
+
export interface User {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
export interface Event {
|
|
3
|
+
// Note: EventModel is mapped to Event and EventExtended via backend_to_ts_model_name_conversions.
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
organizer: User;
|
|
7
|
+
participants?: User[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EventExtended extends Event {
|
|
11
|
+
isPrivate: boolean;
|
|
12
|
+
// ts-backend-check: ignore field date
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface User {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
ts_backend_check/utils.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Utility functions for ts-backend-check.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def snake_to_camel(input_str: str) -> str:
|
|
8
10
|
"""
|
|
@@ -20,7 +22,8 @@ def snake_to_camel(input_str: str) -> str:
|
|
|
20
22
|
|
|
21
23
|
Examples
|
|
22
24
|
--------
|
|
23
|
-
hello_world -> helloWorld
|
|
25
|
+
hello_world -> helloWorld
|
|
26
|
+
alreadyCamelCase -> alreadyCamelCase
|
|
24
27
|
"""
|
|
25
28
|
if not input_str or input_str.startswith("_"):
|
|
26
29
|
return input_str
|
|
@@ -37,3 +40,24 @@ def snake_to_camel(input_str: str) -> str:
|
|
|
37
40
|
result += word[0].upper() + word[1:].lower()
|
|
38
41
|
|
|
39
42
|
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_ordered_subset(reference_list: list[Any], candidate_sub_list: list[Any]) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Return True if candidate elements appear in the same relative order as they do in the reference.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
reference_list : list
|
|
52
|
+
The original list to reference.
|
|
53
|
+
|
|
54
|
+
candidate_sub_list : list
|
|
55
|
+
A potential list that has elements that are in the same relative order to the reference.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
bool
|
|
60
|
+
Whether the candidate elements appear in the same relative order as they do in the reference.
|
|
61
|
+
"""
|
|
62
|
+
it = iter(reference_list)
|
|
63
|
+
return all(item in it for item in candidate_sub_list)
|