37
37
)
38
38
from cadwyn .structure import Version , VersionBundle
39
39
from cadwyn .structure .common import Endpoint , VersionType
40
+ from cadwyn .structure .data import _AlterRequestByPathInstruction , _AlterResponseByPathInstruction
40
41
from cadwyn .structure .endpoints import (
41
42
EndpointDidntExistInstruction ,
42
43
EndpointExistedInstruction ,
43
44
EndpointHadInstruction ,
44
45
)
46
+ from cadwyn .structure .versions import VersionChange
45
47
46
48
if TYPE_CHECKING :
47
49
from fastapi .dependencies .models import Dependant
52
54
_RouteT = TypeVar ("_RouteT" , bound = BaseRoute )
53
55
# This is a hack we do because we can't guarantee how the user will use the router.
54
56
_DELETED_ROUTE_TAG = "_CADWYN_DELETED_ROUTE"
57
+ _RoutePath = str
58
+ _RouteMethod = str
59
+ _RouteId = int
55
60
56
61
57
62
@dataclass (** DATACLASS_SLOTS , frozen = True , eq = True )
@@ -123,6 +128,7 @@ def __init__(self, parent_router: _R, versions: VersionBundle, webhooks: _WR) ->
123
128
]
124
129
125
130
def transform (self ) -> GeneratedRouters [_R , _WR ]:
131
+ # Copy MUST keep the order and number of routes. Otherwise, a ton of code below will break.
126
132
router = copy_router (self .parent_router )
127
133
webhook_router = copy_router (self .parent_webhooks_router )
128
134
routers : dict [VersionType , _R ] = {}
@@ -132,7 +138,7 @@ def transform(self) -> GeneratedRouters[_R, _WR]:
132
138
self .schema_generators [str (version .value )].annotation_transformer .migrate_router_to_version (router )
133
139
self .schema_generators [str (version .value )].annotation_transformer .migrate_router_to_version (webhook_router )
134
140
135
- self ._validate_all_data_converters_are_applied (router , version )
141
+ self ._attach_routes_to_data_converters (router , self . parent_router , version )
136
142
137
143
routers [version .value ] = router
138
144
webhook_routers [version .value ] = webhook_router
@@ -193,9 +199,11 @@ def transform(self) -> GeneratedRouters[_R, _WR]:
193
199
]
194
200
return GeneratedRouters (routers , webhook_routers )
195
201
196
- def _validate_all_data_converters_are_applied (self , router : APIRouter , version : Version ):
197
- path_to_route_methods_mapping , head_response_models , head_request_bodies = self ._extract_all_routes_identifiers (
198
- router
202
+ def _attach_routes_to_data_converters (self , router : APIRouter , head_router : APIRouter , version : Version ):
203
+ # This method is way out of its league in terms of complexity. We gotta refactor it.
204
+
205
+ path_to_route_methods_mapping , head_response_models , head_request_bodies = (
206
+ self ._extract_all_routes_identifiers_for_route_to_converter_matching (router )
199
207
)
200
208
201
209
for version_change in version .changes :
@@ -204,21 +212,10 @@ def _validate_all_data_converters_are_applied(self, router: APIRouter, version:
204
212
* version_change .alter_request_by_path_instructions .values (),
205
213
]:
206
214
for by_path_converter in by_path_converters :
207
- missing_methods = by_path_converter . methods . difference (
208
- path_to_route_methods_mapping [ by_path_converter . path ]
215
+ self . _attach_routes_by_path_converter (
216
+ head_router , path_to_route_methods_mapping , version_change , by_path_converter
209
217
)
210
218
211
- if missing_methods :
212
- raise RouteByPathConverterDoesNotApplyToAnythingError (
213
- f"{ by_path_converter .repr_name } "
214
- f'"{ version_change .__name__ } .{ by_path_converter .transformer .__name__ } " '
215
- f"failed to find routes with the following methods: { list (missing_methods )} . "
216
- f"This means that you are trying to apply this converter to non-existing endpoint(s). "
217
- "Please, check whether the path and methods are correct. (hint: path must include "
218
- "all path variables and have a name that was used in the version that this "
219
- "VersionChange resides in)"
220
- )
221
-
222
219
for by_schema_converters in version_change .alter_request_by_schema_instructions .values ():
223
220
for by_schema_converter in by_schema_converters :
224
221
if not by_schema_converter .check_usage : # pragma: no cover
@@ -249,14 +246,50 @@ def _validate_all_data_converters_are_applied(self, router: APIRouter, version:
249
246
f"{ version_change .__name__ } .{ by_schema_converter .transformer .__name__ } "
250
247
)
251
248
252
- def _extract_all_routes_identifiers (
253
- self , router : APIRouter
254
- ) -> tuple [defaultdict [str , set [str ]], set [Any ], set [Any ]]:
255
- response_models : set [Any ] = set ()
256
- request_bodies : set [Any ] = set ()
257
- path_to_route_methods_mapping : dict [str , set [str ]] = defaultdict (set )
249
+ def _attach_routes_by_path_converter (
250
+ self ,
251
+ head_router : APIRouter ,
252
+ path_to_route_methods_mapping : dict [_RoutePath , dict [_RouteMethod , set [_RouteId ]]],
253
+ version_change : type [VersionChange ],
254
+ by_path_converter : Union [_AlterResponseByPathInstruction , _AlterRequestByPathInstruction ],
255
+ ):
256
+ missing_methods = set ()
257
+ for method in by_path_converter .methods :
258
+ if method in path_to_route_methods_mapping [by_path_converter .path ]:
259
+ for route_index in path_to_route_methods_mapping [by_path_converter .path ][method ]:
260
+ route = head_router .routes [route_index ]
261
+ if isinstance (by_path_converter , _AlterResponseByPathInstruction ):
262
+ version_change ._route_to_response_migration_mapping [id (route )].append (by_path_converter )
263
+ else :
264
+ version_change ._route_to_request_migration_mapping [id (route )].append (by_path_converter )
265
+ else :
266
+ missing_methods .add (method )
267
+
268
+ if missing_methods :
269
+ raise RouteByPathConverterDoesNotApplyToAnythingError (
270
+ f"{ by_path_converter .repr_name } "
271
+ f'"{ version_change .__name__ } .{ by_path_converter .transformer .__name__ } " '
272
+ f"failed to find routes with the following methods: { list (missing_methods )} . "
273
+ f"This means that you are trying to apply this converter to non-existing endpoint(s). "
274
+ "Please, check whether the path and methods are correct. (hint: path must include "
275
+ "all path variables and have a name that was used in the version that this "
276
+ "VersionChange resides in)"
277
+ )
258
278
259
- for route in router .routes :
279
+ def _extract_all_routes_identifiers_for_route_to_converter_matching (
280
+ self , router : APIRouter
281
+ ) -> tuple [dict [_RoutePath , dict [_RouteMethod , set [_RouteId ]]], set [Any ], set [Any ]]:
282
+ # int is the index of the route in the router.routes list.
283
+ # So we essentially keep track of which routes have which response models and request bodies.
284
+ # and their indices in the router.routes list. The indices will allow us to match them to the same
285
+ # routes in the head version. This gives us the ability to later apply changes to these routes
286
+ # without thinking about any renamings or response model changes.
287
+
288
+ response_models = set ()
289
+ request_bodies = set ()
290
+ path_to_route_methods_mapping : dict [str , dict [str , set [int ]]] = defaultdict (lambda : defaultdict (set ))
291
+
292
+ for index , route in enumerate (router .routes ):
260
293
if isinstance (route , APIRoute ):
261
294
if route .response_model is not None and lenient_issubclass (route .response_model , BaseModel ):
262
295
response_models .add (route .response_model )
@@ -265,7 +298,8 @@ def _extract_all_routes_identifiers(
265
298
annotation = route .body_field .field_info .annotation
266
299
if annotation is not None and lenient_issubclass (annotation , BaseModel ):
267
300
request_bodies .add (annotation )
268
- path_to_route_methods_mapping [route .path ] |= route .methods
301
+ for method in route .methods :
302
+ path_to_route_methods_mapping [route .path ][method ].add (index )
269
303
270
304
head_response_models = {model .__cadwyn_original_model__ for model in response_models }
271
305
head_request_bodies = {getattr (body , "__cadwyn_original_model__" , body ) for body in request_bodies }
0 commit comments