@@ -2206,6 +2206,162 @@ def configure_openapi(
22062206 openapi_extensions = openapi_extensions ,
22072207 )
22082208
2209+ def configure_openapi_merge (
2210+ self ,
2211+ path : str ,
2212+ pattern : str | list [str ] = "handler.py" ,
2213+ exclude : list [str ] | None = None ,
2214+ resolver_name : str = "app" ,
2215+ recursive : bool = False ,
2216+ title : str = DEFAULT_OPENAPI_TITLE ,
2217+ version : str = DEFAULT_API_VERSION ,
2218+ openapi_version : str = DEFAULT_OPENAPI_VERSION ,
2219+ summary : str | None = None ,
2220+ description : str | None = None ,
2221+ tags : list [Tag | str ] | None = None ,
2222+ servers : list [Server ] | None = None ,
2223+ terms_of_service : str | None = None ,
2224+ contact : Contact | None = None ,
2225+ license_info : License | None = None ,
2226+ security_schemes : dict [str , SecurityScheme ] | None = None ,
2227+ security : list [dict [str , list [str ]]] | None = None ,
2228+ external_documentation : ExternalDocumentation | None = None ,
2229+ openapi_extensions : dict [str , Any ] | None = None ,
2230+ on_conflict : Literal ["warn" , "error" , "first" , "last" ] = "warn" ,
2231+ ):
2232+ """Configure OpenAPI merge to generate a unified schema from multiple Lambda handlers.
2233+
2234+ This method discovers resolver instances across multiple Python files and merges
2235+ their OpenAPI schemas into a single unified specification. Useful for micro-function
2236+ architectures where each Lambda has its own resolver.
2237+
2238+ Parameters
2239+ ----------
2240+ path : str
2241+ Root directory path to search for resolver files.
2242+ pattern : str | list[str], optional
2243+ Glob pattern(s) to match handler files. Default is "handler.py".
2244+ exclude : list[str], optional
2245+ Patterns to exclude from search. Default excludes tests, __pycache__, and .venv.
2246+ resolver_name : str, optional
2247+ Name of the resolver variable in handler files. Default is "app".
2248+ recursive : bool, optional
2249+ Whether to search recursively in subdirectories. Default is False.
2250+ title : str
2251+ The title of the unified API.
2252+ version : str
2253+ The version of the OpenAPI document.
2254+ openapi_version : str, default = "3.1.0"
2255+ The version of the OpenAPI Specification.
2256+ summary : str, optional
2257+ A short summary of what the application does.
2258+ description : str, optional
2259+ A verbose explanation of the application behavior.
2260+ tags : list[Tag | str], optional
2261+ A list of tags used by the specification with additional metadata.
2262+ servers : list[Server], optional
2263+ An array of Server Objects for connectivity information.
2264+ terms_of_service : str, optional
2265+ A URL to the Terms of Service for the API.
2266+ contact : Contact, optional
2267+ The contact information for the exposed API.
2268+ license_info : License, optional
2269+ The license information for the exposed API.
2270+ security_schemes : dict[str, SecurityScheme], optional
2271+ Security schemes available in the specification.
2272+ security : list[dict[str, list[str]]], optional
2273+ Security mechanisms applied globally across the API.
2274+ external_documentation : ExternalDocumentation, optional
2275+ A link to external documentation for the API.
2276+ openapi_extensions : dict[str, Any], optional
2277+ Additional OpenAPI extensions as a dictionary.
2278+ on_conflict : str, optional
2279+ Strategy for handling conflicts when the same path+method is defined
2280+ in multiple schemas. Options: "warn" (default), "error", "first", "last".
2281+
2282+ Example
2283+ -------
2284+ >>> from aws_lambda_powertools.event_handler import APIGatewayRestResolver
2285+ >>>
2286+ >>> app = APIGatewayRestResolver()
2287+ >>> app.configure_openapi_merge(
2288+ ... path="./functions",
2289+ ... pattern="handler.py",
2290+ ... exclude=["**/tests/**"],
2291+ ... resolver_name="app",
2292+ ... title="My Unified API",
2293+ ... version="1.0.0",
2294+ ... )
2295+
2296+ See Also
2297+ --------
2298+ configure_openapi : Configure OpenAPI for a single resolver
2299+ enable_swagger : Enable Swagger UI
2300+ """
2301+ from aws_lambda_powertools .event_handler .openapi .merge import OpenAPIMerge
2302+
2303+ if exclude is None :
2304+ exclude = ["**/tests/**" , "**/__pycache__/**" , "**/.venv/**" ]
2305+
2306+ self ._openapi_merge = OpenAPIMerge (
2307+ title = title ,
2308+ version = version ,
2309+ openapi_version = openapi_version ,
2310+ summary = summary ,
2311+ description = description ,
2312+ tags = tags ,
2313+ servers = servers ,
2314+ terms_of_service = terms_of_service ,
2315+ contact = contact ,
2316+ license_info = license_info ,
2317+ security_schemes = security_schemes ,
2318+ security = security ,
2319+ external_documentation = external_documentation ,
2320+ openapi_extensions = openapi_extensions ,
2321+ on_conflict = on_conflict ,
2322+ )
2323+ self ._openapi_merge .discover (
2324+ path = path ,
2325+ pattern = pattern ,
2326+ exclude = exclude ,
2327+ resolver_name = resolver_name ,
2328+ recursive = recursive ,
2329+ )
2330+
2331+ def get_openapi_merge_schema (self ) -> dict [str , Any ]:
2332+ """Get the merged OpenAPI schema from multiple Lambda handlers.
2333+
2334+ Returns
2335+ -------
2336+ dict[str, Any]
2337+ The merged OpenAPI schema.
2338+
2339+ Raises
2340+ ------
2341+ RuntimeError
2342+ If configure_openapi_merge has not been called.
2343+ """
2344+ if not hasattr (self , "_openapi_merge" ) or self ._openapi_merge is None :
2345+ raise RuntimeError ("configure_openapi_merge must be called before get_openapi_merge_schema" )
2346+ return self ._openapi_merge .get_openapi_schema ()
2347+
2348+ def get_openapi_merge_json_schema (self ) -> str :
2349+ """Get the merged OpenAPI schema as JSON from multiple Lambda handlers.
2350+
2351+ Returns
2352+ -------
2353+ str
2354+ The merged OpenAPI schema as a JSON string.
2355+
2356+ Raises
2357+ ------
2358+ RuntimeError
2359+ If configure_openapi_merge has not been called.
2360+ """
2361+ if not hasattr (self , "_openapi_merge" ) or self ._openapi_merge is None :
2362+ raise RuntimeError ("configure_openapi_merge must be called before get_openapi_merge_json_schema" )
2363+ return self ._openapi_merge .get_openapi_json_schema ()
2364+
22092365 def enable_swagger (
22102366 self ,
22112367 * ,
@@ -2312,32 +2468,38 @@ def swagger_handler():
23122468
23132469 openapi_servers = servers or [Server (url = (base_path or "/" ))]
23142470
2315- spec = self .get_openapi_schema (
2316- title = title ,
2317- version = version ,
2318- openapi_version = openapi_version ,
2319- summary = summary ,
2320- description = description ,
2321- tags = tags ,
2322- servers = openapi_servers ,
2323- terms_of_service = terms_of_service ,
2324- contact = contact ,
2325- license_info = license_info ,
2326- security_schemes = security_schemes ,
2327- security = security ,
2328- external_documentation = external_documentation ,
2329- openapi_extensions = openapi_extensions ,
2330- )
2471+ # Use merged schema if configure_openapi_merge was called, otherwise use regular schema
2472+ if hasattr (self , "_openapi_merge" ) and self ._openapi_merge is not None :
2473+ # Get merged schema as JSON string (already properly serialized)
2474+ escaped_spec = self ._openapi_merge .get_openapi_json_schema ().replace ("</" , "<\\ /" )
2475+ else :
2476+ spec = self .get_openapi_schema (
2477+ title = title ,
2478+ version = version ,
2479+ openapi_version = openapi_version ,
2480+ summary = summary ,
2481+ description = description ,
2482+ tags = tags ,
2483+ servers = openapi_servers ,
2484+ terms_of_service = terms_of_service ,
2485+ contact = contact ,
2486+ license_info = license_info ,
2487+ security_schemes = security_schemes ,
2488+ security = security ,
2489+ external_documentation = external_documentation ,
2490+ openapi_extensions = openapi_extensions ,
2491+ )
23312492
2332- # The .replace('</', '<\\/') part is necessary to prevent a potential issue where the JSON string contains
2333- # </script> or similar tags. Escaping the forward slash in </ as <\/ ensures that the JSON does not
2334- # inadvertently close the script tag, and the JSON remains a valid string within the JavaScript code.
2335- escaped_spec = model_json (
2336- spec ,
2337- by_alias = True ,
2338- exclude_none = True ,
2339- indent = 2 ,
2340- ).replace ("</" , "<\\ /" )
2493+ # The .replace('</', '<\\/') part is necessary to prevent a potential issue where the JSON
2494+ # string contains </script> or similar tags. Escaping the forward slash in </ as <\/ ensures
2495+ # that the JSON does not inadvertently close the script tag, and the JSON remains a valid
2496+ # string within the JavaScript code.
2497+ escaped_spec = model_json (
2498+ spec ,
2499+ by_alias = True ,
2500+ exclude_none = True ,
2501+ indent = 2 ,
2502+ ).replace ("</" , "<\\ /" )
23412503
23422504 # Check for query parameters; if "format" is specified as "json",
23432505 # respond with the JSON used in the OpenAPI spec
0 commit comments