exdrf-gen-openapi2rtk 0.1.17__tar.gz

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.
Files changed (25) hide show
  1. exdrf_gen_openapi2rtk-0.1.17/PKG-INFO +80 -0
  2. exdrf_gen_openapi2rtk-0.1.17/README.md +48 -0
  3. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/__init__.py +77 -0
  4. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/__version__.py +24 -0
  5. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/creator.py +314 -0
  6. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/endpoint_keys.py +29 -0
  7. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi2rtk_templates/openapi2rtk/api.ts.j2 +112 -0
  8. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi2rtk_templates/openapi2rtk/base-api.fr_one.ts.j2 +61 -0
  9. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi2rtk_templates/openapi2rtk/base-api.minimal.ts.j2 +40 -0
  10. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi2rtk_templates/openapi2rtk/cacheUtils.ts.j2 +58 -0
  11. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi2rtk_templates/openapi2rtk/index.ts.j2 +17 -0
  12. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi2rtk_templates/openapi2rtk/list-query-contract.ts.j2 +37 -0
  13. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/openapi_cache.py +151 -0
  14. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/py.typed +0 -0
  15. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk/spec_routes.py +415 -0
  16. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk.egg-info/PKG-INFO +80 -0
  17. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk.egg-info/SOURCES.txt +23 -0
  18. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk.egg-info/dependency_links.txt +1 -0
  19. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk.egg-info/entry_points.txt +2 -0
  20. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk.egg-info/requires.txt +20 -0
  21. exdrf_gen_openapi2rtk-0.1.17/exdrf_gen_openapi2rtk.egg-info/top_level.txt +3 -0
  22. exdrf_gen_openapi2rtk-0.1.17/pyproject.toml +69 -0
  23. exdrf_gen_openapi2rtk-0.1.17/setup.cfg +4 -0
  24. exdrf_gen_openapi2rtk-0.1.17/tests/openapi2rtk_cli_test.py +50 -0
  25. exdrf_gen_openapi2rtk-0.1.17/tests/spec_routes_opid_test.py +27 -0
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: exdrf-gen-openapi2rtk
3
+ Version: 0.1.17
4
+ Summary: Generate RTK Query TypeScript from OpenAPI (cached JSON).
5
+ Author-email: Nicu Tofan <nicu.tofan@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.12.2
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: attrs
14
+ Requires-Dist: click<9,>=8.2.1
15
+ Requires-Dist: exdrf-gen>=0.1.17
16
+ Requires-Dist: exdrf-ts
17
+ Provides-Extra: dev
18
+ Requires-Dist: autoflake; extra == "dev"
19
+ Requires-Dist: black; extra == "dev"
20
+ Requires-Dist: build; extra == "dev"
21
+ Requires-Dist: flake8; extra == "dev"
22
+ Requires-Dist: isort; extra == "dev"
23
+ Requires-Dist: mypy; extra == "dev"
24
+ Requires-Dist: pre-commit; extra == "dev"
25
+ Requires-Dist: pyproject-flake8; extra == "dev"
26
+ Requires-Dist: pytest-cov; extra == "dev"
27
+ Requires-Dist: pytest-mock; extra == "dev"
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: twine; extra == "dev"
30
+ Requires-Dist: wheel; extra == "dev"
31
+ Requires-Dist: click<9,>=8.2.1; extra == "dev"
32
+
33
+ # exdrf-gen-openapi2rtk
34
+
35
+ **exdrf-gen-openapi2rtk** is an **`exdrf-gen`** plugin that turns an **OpenAPI
36
+ 3.x** JSON document into **RTK Query** TypeScript modules (endpoints, hooks, and
37
+ generated request/response wiring). It can load a local file or fetch a remote
38
+ spec with optional HTTP caching.
39
+
40
+ Python **3.12.2+** is required. Dependencies (**`attrs`**, **`click`**,
41
+ **`exdrf-gen`**, **`exdrf-ts`**) are listed in `pyproject.toml`.
42
+
43
+ ## Plugin registration
44
+
45
+ ```toml
46
+ [project.entry-points.'exdrf.plugins']
47
+ exdrf_gen = 'exdrf_gen_openapi2rtk'
48
+ ```
49
+
50
+ Install **`exdrf-gen`** and **`exdrf-gen-openapi2rtk`** in the same environment;
51
+ then **`exdrf-gen --help`** lists **`openapi2rtk`**.
52
+
53
+ ## Usage
54
+
55
+ ### CLI
56
+
57
+ The command is registered on the shared **`exdrf-gen`** CLI as
58
+ **`openapi2rtk`**:
59
+
60
+ ```bash
61
+ python -m exdrf_gen openapi2rtk /path/to/routes/out \
62
+ --openapi-file /path/to/openapi.json \
63
+ --types-import "@app/models" \
64
+ --base-api-profile minimal
65
+ ```
66
+
67
+ Use **`--base-api-profile fr_one`** when generating for the **fr-one** customer
68
+ SPA (same wiring as historical **`resi_gen r2ts`** output).
69
+
70
+ Remote specs: pass **`--openapi-url`** (and optional cache flags) instead of or
71
+ in addition to **`--openapi-file`**—see **`exdrf-gen --help`** / subcommand help
72
+ for the exact options in your installed version.
73
+
74
+ ## See also
75
+
76
+ - **`resi_gen r2ts`** in **bk-one** snapshots **`app.openapi()`**, then calls
77
+ this generator with **`--types-import @resi/models`** and **`fr_one`** base
78
+ API.
79
+ - **`exdrf-gen`** — shared CLI, Jinja environment, and plugin loading.
80
+ - **`exdrf-ts`** — type mapping helpers used during emission.
@@ -0,0 +1,48 @@
1
+ # exdrf-gen-openapi2rtk
2
+
3
+ **exdrf-gen-openapi2rtk** is an **`exdrf-gen`** plugin that turns an **OpenAPI
4
+ 3.x** JSON document into **RTK Query** TypeScript modules (endpoints, hooks, and
5
+ generated request/response wiring). It can load a local file or fetch a remote
6
+ spec with optional HTTP caching.
7
+
8
+ Python **3.12.2+** is required. Dependencies (**`attrs`**, **`click`**,
9
+ **`exdrf-gen`**, **`exdrf-ts`**) are listed in `pyproject.toml`.
10
+
11
+ ## Plugin registration
12
+
13
+ ```toml
14
+ [project.entry-points.'exdrf.plugins']
15
+ exdrf_gen = 'exdrf_gen_openapi2rtk'
16
+ ```
17
+
18
+ Install **`exdrf-gen`** and **`exdrf-gen-openapi2rtk`** in the same environment;
19
+ then **`exdrf-gen --help`** lists **`openapi2rtk`**.
20
+
21
+ ## Usage
22
+
23
+ ### CLI
24
+
25
+ The command is registered on the shared **`exdrf-gen`** CLI as
26
+ **`openapi2rtk`**:
27
+
28
+ ```bash
29
+ python -m exdrf_gen openapi2rtk /path/to/routes/out \
30
+ --openapi-file /path/to/openapi.json \
31
+ --types-import "@app/models" \
32
+ --base-api-profile minimal
33
+ ```
34
+
35
+ Use **`--base-api-profile fr_one`** when generating for the **fr-one** customer
36
+ SPA (same wiring as historical **`resi_gen r2ts`** output).
37
+
38
+ Remote specs: pass **`--openapi-url`** (and optional cache flags) instead of or
39
+ in addition to **`--openapi-file`**—see **`exdrf-gen --help`** / subcommand help
40
+ for the exact options in your installed version.
41
+
42
+ ## See also
43
+
44
+ - **`resi_gen r2ts`** in **bk-one** snapshots **`app.openapi()`**, then calls
45
+ this generator with **`--types-import @resi/models`** and **`fr_one`** base
46
+ API.
47
+ - **`exdrf-gen`** — shared CLI, Jinja environment, and plugin loading.
48
+ - **`exdrf-ts`** — type mapping helpers used during emission.
@@ -0,0 +1,77 @@
1
+ """exdrf-gen plugin: RTK Query TypeScript from OpenAPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import click
8
+ from click import Context
9
+
10
+ from exdrf_gen.cli_base import cli
11
+ from exdrf_gen.plugin_support import install_plugin
12
+ from exdrf_gen_openapi2rtk.creator import run_openapi2rtk
13
+
14
+ install_plugin(
15
+ template_paths=[
16
+ os.path.join(os.path.dirname(__file__), "openapi2rtk_templates"),
17
+ ],
18
+ )
19
+
20
+
21
+ @cli.command(name="openapi2rtk")
22
+ @click.argument(
23
+ "routes_out_dir",
24
+ type=click.Path(file_okay=False, dir_okay=True),
25
+ envvar="EXDRF_OPENAPI2RTK_ROUTES_DIR",
26
+ )
27
+ @click.option(
28
+ "--openapi-file",
29
+ type=click.Path(exists=True, dir_okay=False),
30
+ default=None,
31
+ help="Path to a local openapi.json (or use --openapi-url).",
32
+ )
33
+ @click.option(
34
+ "--openapi-url",
35
+ default=None,
36
+ help="HTTP(S) URL to fetch OpenAPI JSON (requires --cache-file).",
37
+ )
38
+ @click.option(
39
+ "--cache-file",
40
+ type=click.Path(dir_okay=False),
41
+ default=None,
42
+ help="Cache path for --openapi-url downloads and ETag metadata.",
43
+ )
44
+ @click.option(
45
+ "--types-import",
46
+ default="@resi/models",
47
+ show_default=True,
48
+ help="Module path passed to emitted ``from '…'`` type imports.",
49
+ )
50
+ @click.option(
51
+ "--base-api-profile",
52
+ type=click.Choice(["minimal", "fr_one"]),
53
+ default="minimal",
54
+ show_default=True,
55
+ help=("``fr_one`` matches fr-one auth/baseUrl wiring; ``minimal`` is generic."),
56
+ )
57
+ @click.pass_context
58
+ def openapi2rtk(
59
+ context: Context,
60
+ routes_out_dir: str,
61
+ openapi_file: str | None,
62
+ openapi_url: str | None,
63
+ cache_file: str | None,
64
+ types_import: str,
65
+ base_api_profile: str,
66
+ ) -> None:
67
+ """Generate RTK Query route modules from an OpenAPI document."""
68
+
69
+ run_openapi2rtk(
70
+ context,
71
+ routes_out_dir,
72
+ openapi_file,
73
+ openapi_url,
74
+ cache_file,
75
+ types_import,
76
+ base_api_profile,
77
+ )
@@ -0,0 +1,24 @@
1
+ """Package version from PEP 621 or installed metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from exdrf.pep621_version import distribution_version, version_tuple_from_string
8
+
9
+ _PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
10
+ _DIST_NAME = "exdrf-gen-openapi2rtk"
11
+
12
+ __version__ = version = distribution_version(_DIST_NAME, _PYPROJECT)
13
+ __version_tuple__ = version_tuple = version_tuple_from_string(__version__)
14
+
15
+ __commit_id__ = commit_id = None
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "__version_tuple__",
20
+ "version",
21
+ "version_tuple",
22
+ "__commit_id__",
23
+ "commit_id",
24
+ ]
@@ -0,0 +1,314 @@
1
+ """Emit RTK Query TypeScript from a parsed OpenAPI document."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, Mapping
10
+
11
+ import click
12
+ from attrs import define, field
13
+
14
+ from exdrf_gen.fs_support import Base, Dir, File
15
+ from exdrf_gen_openapi2rtk.endpoint_keys import assert_unique_rtk_endpoint_keys
16
+ from exdrf_gen_openapi2rtk.openapi_cache import (
17
+ fetch_openapi_url_cached,
18
+ load_openapi_from_file,
19
+ )
20
+ from exdrf_gen_openapi2rtk.spec_routes import (
21
+ GenRoute,
22
+ routes_by_primary_tag,
23
+ tag_file_stem,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from jinja2 import Environment
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _IN_STATE = {"tenant": "Tenant"}
32
+
33
+
34
+ def _restrict_templates_to_package(env: "Environment", suffix: str) -> None:
35
+ """Limit Jinja search paths to this package's template directory."""
36
+
37
+ loader = getattr(env, "loader", None)
38
+ if loader is None:
39
+ return
40
+ paths = list(getattr(loader, "paths", []))
41
+ filtered = [p for p in paths if str(p).endswith(suffix)]
42
+ setattr(loader, "paths", filtered)
43
+
44
+
45
+ def _category_camel(tag: str) -> str:
46
+ """Derive ``c_camel`` from a primary OpenAPI tag string."""
47
+
48
+ if not tag:
49
+ return tag
50
+ return tag[0].lower() + tag[1:]
51
+
52
+
53
+ def _used_schema_names(routes: list[GenRoute]) -> list[str]:
54
+ """Collect named schema tokens imported from ``types_import``."""
55
+
56
+ names: set[str] = set()
57
+ for r in routes:
58
+ if r.body_m:
59
+ names.add(r.body_m.name)
60
+ if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", r.response_ts):
61
+ names.add(r.response_ts)
62
+ return sorted(names)
63
+
64
+
65
+ def _build_api_bundle(
66
+ category: str,
67
+ routes: list[GenRoute],
68
+ *,
69
+ types_import: str,
70
+ m_name: str,
71
+ ) -> dict[str, Any]:
72
+ """Assemble the Jinja context for one per-tag ``*.ts`` module."""
73
+
74
+ assert_unique_rtk_endpoint_keys(category, routes)
75
+
76
+ c_snake = tag_file_stem(category)
77
+ c_camel = _category_camel(category)
78
+ uses_list_query_contract = any(
79
+ r.body_schema_name == "ListQueryRequest" for r in routes
80
+ )
81
+ used_res_names = _used_schema_names(routes)
82
+
83
+ global_sourced = {
84
+ **{
85
+ arg: f"(api.getState() as LocalState).runtime.{arg}.id" for arg in _IN_STATE
86
+ },
87
+ }
88
+
89
+ def has_local_state(rs: list[GenRoute]) -> bool:
90
+ """True when any route body references ``_IN_STATE`` keys."""
91
+
92
+ for r in rs:
93
+ if r.body_m:
94
+ for fld in r.body_m.fields:
95
+ if fld.name in _IN_STATE:
96
+ return True
97
+ return False
98
+
99
+ def get_local_state(fld: Any) -> str:
100
+ """Map a body field to its global-state TypeScript type label."""
101
+
102
+ return _IN_STATE[fld.name]
103
+
104
+ def get_arg(arg: str) -> str:
105
+ """Build the JavaScript expression for one request argument."""
106
+
107
+ if arg in global_sourced:
108
+ return global_sourced[arg]
109
+ return "args." + arg
110
+
111
+ arg_pattern = re.compile(r"\{([^}]+)\}")
112
+
113
+ def get_route(route: GenRoute) -> str:
114
+ """Interpolate path and query template literals for RTK ``url``."""
115
+
116
+ result = arg_pattern.sub(
117
+ lambda m: "${" + get_arg(m.group(1)) + "}",
118
+ route.path,
119
+ )
120
+ if route.query_args:
121
+ result += "?" + "&".join(
122
+ q[0] + "=${" + get_arg(q[0]) + "}" for q in route.query_args
123
+ )
124
+ return result
125
+
126
+ def get_arg_type(route: GenRoute) -> str:
127
+ """Return the precomputed RTK args type for ``route``."""
128
+
129
+ return route.arg_type_ts
130
+
131
+ def get_response_type(route: GenRoute) -> str:
132
+ """Return the precomputed RTK response type for ``route``."""
133
+
134
+ return route.response_ts
135
+
136
+ def get_body_arg_names(route: GenRoute) -> list[str]:
137
+ """Return JSON body property names for ``route``."""
138
+
139
+ if route.body_m:
140
+ return [f.name for f in route.body_m.fields]
141
+ return [n for n, _ in route.body_args]
142
+
143
+ kind_str = ["mutation" if r.is_mutation else "query" for r in routes]
144
+
145
+ return {
146
+ "tag_snake": c_snake,
147
+ "category": category,
148
+ "routes": routes,
149
+ "c_snake": c_snake,
150
+ "c_camel": c_camel,
151
+ "r_count": len(routes),
152
+ "r_snake": [r.name_snake_case for r in routes],
153
+ "r_camel": [r.name_camel for r in routes],
154
+ "r_pascal": [r.name_pascal for r in routes],
155
+ "uses_list_query_contract": uses_list_query_contract,
156
+ "used_res_names": used_res_names,
157
+ "used_res": {n: None for n in used_res_names},
158
+ "kind_str": kind_str,
159
+ "global_sourced": global_sourced,
160
+ "get_arg": get_arg,
161
+ "get_route": get_route,
162
+ "get_arg_type": get_arg_type,
163
+ "get_response_type": get_response_type,
164
+ "get_local_state": get_local_state,
165
+ "has_local_state": has_local_state,
166
+ "get_body_arg_names": get_body_arg_names,
167
+ "in_state": _IN_STATE,
168
+ "types_import": types_import,
169
+ "m_name": m_name,
170
+ }
171
+
172
+
173
+ @define
174
+ class _RoutesPackageDir(Base):
175
+ """Emit ``index.ts`` plus one ``{tag_snake}.ts`` file per OpenAPI tag."""
176
+
177
+ name: str = field()
178
+ bundles: list[dict[str, Any]] = field(factory=list)
179
+ extra: dict[str, Any] = field(factory=dict)
180
+
181
+ def generate(self, out_path: str, **kwargs: Any) -> None:
182
+ """Create the routes directory and all per-tag RTK modules."""
183
+
184
+ mapping = {**self.extra, **kwargs}
185
+ c_path = self.create_directory(out_path, self.name, **mapping)
186
+ env = mapping["env"]
187
+ child_kw = {k: v for k, v in mapping.items() if k != "env"}
188
+ File("index.ts", "openapi2rtk/index.ts.j2").generate(
189
+ c_path,
190
+ env=env,
191
+ **child_kw,
192
+ )
193
+ for b in self.bundles:
194
+ File("{tag_snake}.ts", "openapi2rtk/api.ts.j2").generate(
195
+ c_path,
196
+ env=env,
197
+ **{**child_kw, **b},
198
+ )
199
+
200
+
201
+ def generate_openapi2rtk(
202
+ spec: Mapping[str, Any],
203
+ routes_out_dir: str,
204
+ env: "Environment",
205
+ *,
206
+ types_import: str,
207
+ base_api_profile: str,
208
+ m_name: str,
209
+ ) -> None:
210
+ """Write RTK route modules and shared helpers next to ``routes_out_dir``."""
211
+
212
+ by_category = routes_by_primary_tag(spec)
213
+ if not by_category:
214
+ logger.warning(
215
+ "OpenAPI document produced no tagged operations; no TS files.",
216
+ )
217
+
218
+ bundles: list[dict[str, Any]] = []
219
+ for cat in sorted(by_category):
220
+ routes = by_category[cat]
221
+ if not routes:
222
+ continue
223
+ bundles.append(
224
+ _build_api_bundle(
225
+ cat,
226
+ routes,
227
+ types_import=types_import,
228
+ m_name=m_name,
229
+ )
230
+ )
231
+
232
+ c_keys = sorted(by_category.keys())
233
+ c_camel = [_category_camel(c) for c in c_keys]
234
+ c_snake = [tag_file_stem(c) for c in c_keys]
235
+ glb_vars: dict[str, Any] = {
236
+ "c_count": len(c_keys),
237
+ "c_keys": c_keys,
238
+ "c_snake": c_snake,
239
+ "c_camel": c_camel,
240
+ "categories": by_category,
241
+ "m_name": m_name,
242
+ }
243
+
244
+ routes_path = Path(routes_out_dir)
245
+ parent = str(routes_path.parent)
246
+ routes_folder_name = routes_path.name
247
+
248
+ base_tpl = (
249
+ "openapi2rtk/base-api.fr_one.ts.j2"
250
+ if base_api_profile == "fr_one"
251
+ else "openapi2rtk/base-api.minimal.ts.j2"
252
+ )
253
+
254
+ root = Dir(
255
+ name="",
256
+ comp=[
257
+ File(
258
+ "list-query-contract.ts",
259
+ "openapi2rtk/list-query-contract.ts.j2",
260
+ ),
261
+ File("base-api.ts", base_tpl),
262
+ File("cacheUtils.ts", "openapi2rtk/cacheUtils.ts.j2"),
263
+ _RoutesPackageDir(
264
+ name=routes_folder_name,
265
+ bundles=bundles,
266
+ ),
267
+ ],
268
+ )
269
+ root.generate(
270
+ parent,
271
+ env=env,
272
+ **glb_vars,
273
+ )
274
+
275
+
276
+ def run_openapi2rtk(
277
+ context: click.Context,
278
+ routes_out_dir: str,
279
+ openapi_file: str | None,
280
+ openapi_url: str | None,
281
+ cache_file: str | None,
282
+ types_import: str,
283
+ base_api_profile: str,
284
+ ) -> None:
285
+ """CLI entry: load spec, then emit RTK TypeScript."""
286
+
287
+ env = context.obj["jinja_env"]
288
+ _restrict_templates_to_package(env, "openapi2rtk_templates")
289
+
290
+ if openapi_file:
291
+ spec = load_openapi_from_file(openapi_file)
292
+ elif openapi_url:
293
+ if not cache_file:
294
+ click.echo(
295
+ "--cache-file is required when using --openapi-url.",
296
+ err=True,
297
+ )
298
+ sys.exit(1)
299
+ spec = fetch_openapi_url_cached(openapi_url, Path(cache_file))
300
+ else:
301
+ click.echo(
302
+ "Provide --openapi-file or --openapi-url (with --cache-file).",
303
+ err=True,
304
+ )
305
+ sys.exit(1)
306
+
307
+ generate_openapi2rtk(
308
+ spec,
309
+ routes_out_dir,
310
+ env,
311
+ types_import=types_import,
312
+ base_api_profile=base_api_profile,
313
+ m_name="exdrf_gen_openapi2rtk.creator",
314
+ )
@@ -0,0 +1,29 @@
1
+ """RTK ``injectEndpoints`` key uniqueness for OpenAPI-derived routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def assert_unique_rtk_endpoint_keys(category: str, routes: list[Any]) -> None:
9
+ """Fail fast when two routes would share the same RTK endpoint object key.
10
+
11
+ Args:
12
+ category: OpenAPI primary tag / grouping label for the emitted file.
13
+ routes: Route objects with ``name_camel`` (camelCase operation key).
14
+
15
+ Raises:
16
+ ValueError: When two routes share the same ``name_camel``.
17
+ """
18
+
19
+ seen: dict[str, str] = {}
20
+ for route in routes:
21
+ key = route.name_camel
22
+ if key in seen:
23
+ raise ValueError(
24
+ "Duplicate RTK endpoint key %r in category %r: paths %r vs %r. "
25
+ "Adjust ``operationId`` values or OpenAPI tags so keys are "
26
+ "unique within each emitted module."
27
+ % (key, category, seen[key], getattr(route, "path", "?"))
28
+ )
29
+ seen[key] = getattr(route, "path", "?")
@@ -0,0 +1,112 @@
1
+ // This file was automatically generated using exdrf-gen-openapi2rtk.
2
+ // Source: {{ m_name }}
3
+ // Don't change it manually.
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ import { BaseQueryApi } from '@reduxjs/toolkit/query';
6
+ import { apiBase } from '../base-api';
7
+ import {
8
+ providesNestedList,
9
+ cacheByIdArgProperty,
10
+ invalidateByIdArgPropertyAndList,
11
+ } from '../cacheUtils';
12
+ {%- if uses_list_query_contract %}
13
+ import type { ListQueryRequest } from '../list-query-contract';
14
+ {%- endif %}
15
+ {%- for res_name in used_res_names %}
16
+ import { {{ res_name }} } from '{{ types_import }}';
17
+ {%- endfor %}
18
+
19
+ {%- if has_local_state(routes) -%}
20
+ {% set name_seen = {} %}
21
+
22
+ export interface LocalState {
23
+ runtime: {
24
+ {%- for i in range(r_count) -%}
25
+ {%- for field in routes[i].body_m.fields -%}
26
+ {%- if field.name in in_state and field.name not in name_seen -%}
27
+ {%- set _ = name_seen.update({field.name: true}) %}
28
+ {{ field.name }}: {{ get_local_state(field) }};
29
+ {% endif -%}
30
+ {%- endfor -%}
31
+ {%- endfor %}
32
+ }
33
+ }
34
+ {% endif %}
35
+
36
+ apiBase.enhanceEndpoints({
37
+ addTagTypes: ['{{ category.capitalize() }}']
38
+ });
39
+
40
+
41
+ export const {{ c_camel }}Api = apiBase.injectEndpoints({
42
+ endpoints: (build) => ({
43
+ {%- for i in range(r_count) %}
44
+
45
+ /**
46
+ {% for line in routes[i].doc_lines %} * {{ line }}
47
+ {% endfor %} */
48
+ {{ r_camel[i] }}: build.{{ kind_str[i] }}<{{ get_response_type(routes[i]) }}, {{
49
+ get_arg_type(routes[i])
50
+ }}>({
51
+ queryFn: async (
52
+ args,
53
+ api: BaseQueryApi,
54
+ extraOptions: any, // eslint-disable-line
55
+ usingBase
56
+ ) => {
57
+ return usingBase({
58
+ url: `{{ get_route(routes[i]) }}`,
59
+ method: '{{ routes[i].method.name }}',
60
+ {%- if routes[i].body_m %}
61
+ body: {
62
+ {%- for arg_name in get_body_arg_names(routes[i]) %}
63
+ {{ arg_name }}: {{ get_arg(arg_name) }},
64
+ {%- endfor %}
65
+ },
66
+ {%- elif routes[i].body_args %}
67
+ body: {
68
+ {%- for arg_name in get_body_arg_names(routes[i]) %}
69
+ {{ arg_name }}: {{ get_arg(arg_name) }},
70
+ {%- endfor %}
71
+ },
72
+ {%- endif %}
73
+ });
74
+ },
75
+ {% if r_camel[i].startswith('create') -%}
76
+ invalidatesTags: [
77
+ { type: '{{ category.capitalize() }}', id: 'LIST' },
78
+ { type: '{{ category.capitalize() }}', id: 'Q-LIST' }
79
+ ] as any,
80
+ {%- elif r_camel[i].startswith('remove') -%}
81
+ invalidatesTags: [
82
+ { type: '{{ category.capitalize() }}', id: 'LIST' },
83
+ { type: '{{ category.capitalize() }}', id: 'Q-LIST' }
84
+ ] as any,
85
+ {%- elif r_camel[i].startswith('listQuick') -%}
86
+ providesTags: [{ type: '{{ category.capitalize() }}', id: 'Q-LIST' }] as any,
87
+ {%- elif routes[i].name.startswith('query_') -%}
88
+ providesTags: providesNestedList('{{ category.capitalize() }}') as any,
89
+ {%- elif r_camel[i].startswith('list') -%}
90
+ providesTags: providesNestedList('{{ category.capitalize() }}') as any,
91
+ {%- elif r_camel[i].startswith('read') -%}
92
+ providesTags: cacheByIdArgProperty('{{ category.capitalize() }}') as any,
93
+ {%- elif r_camel[i].startswith('get') -%}
94
+ providesTags: cacheByIdArgProperty('{{ category.capitalize() }}') as any,
95
+ {%- elif r_camel[i].startswith('update') -%}
96
+ invalidatesTags: invalidateByIdArgPropertyAndList('{{ category.capitalize() }}') as any,
97
+ {%- endif %}
98
+ }),
99
+ {%- endfor %}
100
+ }),
101
+ overrideExisting: false,
102
+ });{{'\n'}}
103
+
104
+
105
+ export const {
106
+ {%- for i in range(r_count) %}
107
+ use{{ r_pascal[i] }}{{ kind_str[i] | capitalize }},
108
+ {%- if kind_str[i] == 'query' %}
109
+ useLazy{{ r_pascal[i] }}{{ kind_str[i] | capitalize }},
110
+ {%- endif -%}
111
+ {%- endfor %}
112
+ } = {{ c_camel }}Api;