@@ -163,6 +163,29 @@ def __init__(
163163 self .variables = variables
164164
165165
166+ class Resource (object ):
167+ """REST API resource."""
168+
169+ def __init__ (
170+ self ,
171+ path : str ,
172+ method_list : list [Method ],
173+ resource_list : list [Resource ] | None = None ,
174+ lambda_arn : str | GetAtt | Ref | None = None ,
175+ ) -> None :
176+ """Initialize a REST API resource.
177+
178+ :param path: the last path segment for this resource
179+ :param method_list: a list of methods accepted on this resource
180+ :param resource_list: a list of child resources
181+ :param lambda_arn: arn of the lambda executed for this resource
182+ """
183+ self .path = path
184+ self .method_list = method_list
185+ self .resource_list = resource_list
186+ self .lambda_arn = lambda_arn
187+
188+
166189class Api (Construct ):
167190 """API abstact Class for APIGateways V1 and V2."""
168191
@@ -679,9 +702,10 @@ def __init__(
679702 name : str ,
680703 description : str ,
681704 lambda_arn : str | GetAtt | Ref ,
682- method_list : list [Method ],
705+ method_list : list [Method ] | None = None ,
683706 burst_limit : int = 10 ,
684707 rate_limit : int = 10 ,
708+ resource_list : list [Resource ] | None = None ,
685709 domain_name : str | None = None ,
686710 hosted_zone_id : str | None = None ,
687711 stages_config : list [StageConfiguration ] | None = None ,
@@ -713,6 +737,7 @@ def __init__(
713737 :param burst_limit: maximum concurrent requests at a given time
714738 (exceeding that limit will cause API Gateway to return 429)
715739 :param rate_limit: maximum number of requests per seconds
740+ :param resource_list: a list of resources to declare
716741 :param domain_name: if domain_name is not None then associate the API
717742 with a given domain name. In that case a certificate is
718743 automatically created for that domain name. Note that if a domain
@@ -738,20 +763,22 @@ def __init__(
738763 stages_config = stages_config ,
739764 )
740765 self .method_list = method_list
741- self .integration_uri = (
742- integration_uri
743- if integration_uri is not None
744- else Sub (
745- "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31"
746- "/functions/${lambdaArn}/invocations" ,
747- dict_values = {"lambdaArn" : lambda_arn },
748- )
749- )
766+ self .integration_uri = integration_uri
750767 assert iam_path .startswith ("/" ), "iam_path must start with '/'"
751768 assert iam_path .endswith ("/" ), "iam_path must end with '/'"
752769 self .iam_path = iam_path
753770 self .policy = policy
754771
772+ # For backward compatibility
773+ if resource_list is None :
774+ assert (
775+ self .method_list is not None
776+ ), "method_list can't be None when resource_list is None"
777+ # Add a default root resource to match everything
778+ resource_list = [Resource (path = "{proxy+}" , method_list = self .method_list )]
779+
780+ self .resource_list = resource_list
781+
755782 def add_cognito_authorizer (
756783 # we ignore the incompatible signature mypy errors
757784 self ,
@@ -793,17 +820,21 @@ def declare_stage(
793820 """
794821 result = []
795822
823+ # Get the list of methods for DependsOn
824+ method_list = [
825+ r
826+ for r in self ._declare_resources (resource_list = self .resource_list )
827+ if isinstance (r , apigateway .Method )
828+ ]
829+
796830 # create deployment resource
797831 deployment_name = self .logical_id + name_to_id (stage_name ) + "Deployment"
798832 result .append (
799833 apigateway .Deployment (
800834 deployment_name ,
801835 Description = f"Deployment resource of { stage_name } stage" ,
802836 RestApiId = Ref (self .logical_id ),
803- DependsOn = [
804- name_to_id (self .name + method .method + "Method" )
805- for method in self .method_list
806- ],
837+ DependsOn = [m .name for m in method_list ],
807838 )
808839 )
809840
@@ -855,24 +886,49 @@ def declare_stage(
855886
856887 return result
857888
858- def declare_method (self , method : Method , resource_id : Ref ) -> list [AWSObject ]:
889+ def _declare_method (
890+ self ,
891+ method : Method ,
892+ resource : Resource ,
893+ resource_id_prefix : str ,
894+ resource_path : str ,
895+ ) -> list [AWSObject ]:
859896 """Declare a method.
860897
861898 :param method: the method definition
899+ :param resource: resource associated with the method
900+ :param resource_id_prefix: resource_id without trailing Resource
901+ :param resource_path: absolute path to the resource
862902 :return: a list of AWSObjects to be added to the stack
863903 """
864904 result = []
865- id_prefix = name_to_id (self .name + method .method )
905+ id_prefix = name_to_id (f"{ resource_id_prefix } -{ method .method } " )
906+
907+ # Take the global lambda_arn or the one configured for the resource
908+ lambda_arn = (
909+ self .lambda_arn if resource .lambda_arn is None else resource .lambda_arn
910+ )
911+
912+ # Integration URI for the resource
913+ integration_uri = (
914+ self .integration_uri
915+ if self .integration_uri is not None
916+ else Sub (
917+ "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31"
918+ "/functions/${lambdaArn}/invocations" ,
919+ dict_values = {"lambdaArn" : lambda_arn },
920+ )
921+ )
866922
867923 integration = apigateway .Integration (
868- id_prefix + " Integration" ,
924+ f" { id_prefix } Integration" ,
869925 # set at POST because we are doing lambda integration
870926 CacheKeyParameters = [],
871927 CacheNamespace = "none" ,
872928 IntegrationHttpMethod = "POST" ,
873929 PassthroughBehavior = "NEVER" ,
874930 Type = "AWS_PROXY" ,
875- Uri = self . integration_uri ,
931+ Uri = integration_uri ,
876932 )
877933
878934 method_params = {
@@ -882,12 +938,12 @@ def declare_method(self, method: Method, resource_id: Ref) -> list[AWSObject]:
882938 else "NONE" ,
883939 "HttpMethod" : f"{ method .method } " ,
884940 "Integration" : integration ,
885- "ResourceId" : resource_id ,
941+ "ResourceId" : Ref ( name_to_id ( f" { resource_id_prefix } Resource" )) ,
886942 }
887943 if method .authorizer_name :
888944 method_params ["AuthorizerId" ] = Ref (name_to_id (method .authorizer_name ))
889945
890- result .append (apigateway .Method (id_prefix + " Method" , ** method_params ))
946+ result .append (apigateway .Method (f" { id_prefix } Method" , ** method_params ))
891947
892948 for config in self .stages_config :
893949 result .append (
@@ -899,11 +955,11 @@ def declare_method(self, method: Method, resource_id: Ref) -> list[AWSObject]:
899955 )
900956 ),
901957 Action = "lambda:InvokeFunction" ,
902- FunctionName = self . lambda_arn ,
958+ FunctionName = lambda_arn ,
903959 Principal = "apigateway.amazonaws.com" ,
904960 SourceArn = Sub (
905961 "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:"
906- f"${{api}}/{ config .name } /${{method}}/* " ,
962+ f"${{api}}/{ config .name } /${{method}}/{ resource_path } " ,
907963 dict_values = {
908964 "api" : self .ref ,
909965 "method" : method .method ,
@@ -961,6 +1017,70 @@ def _declare_api_mapping(
9611017 )
9621018 return result
9631019
1020+ def _declare_resources (
1021+ self ,
1022+ resource_list : list [Resource ],
1023+ parent_id_prefix : str | None = None ,
1024+ parent_path : str | None = None ,
1025+ ) -> list [AWSObject ]:
1026+ """Create API resources and methods recursively.
1027+
1028+ Each resource can define its own methods and have child resources.
1029+
1030+ :param resource_list: list of resources
1031+ :param parent_id_prefix: id of the parent resource without trailing Resource
1032+ :return: a list of AWSObjects to be added to the stack
1033+ """
1034+ result : list [AWSObject ] = []
1035+
1036+ for r in resource_list :
1037+ # Append the path of this resource to the id of the parent resource.
1038+ # Use the API id in case there is no parent.
1039+ # Special {proxy+} case for backward compatibility
1040+ resource_id_prefix = name_to_id (
1041+ "{}{}" .format (
1042+ self .logical_id if parent_id_prefix is None else parent_id_prefix ,
1043+ "" if r .path == "{proxy+}" else f"-{ r .path } " ,
1044+ )
1045+ )
1046+
1047+ # Append the path of this resource to the path of the parent resource
1048+ resource_path = "{}{}" .format (
1049+ "" if parent_path is None else f"{ parent_path } /" ,
1050+ "*" if r .path == "{proxy+}" else r .path ,
1051+ )
1052+
1053+ # Declare the resource
1054+ resource = apigateway .Resource (
1055+ f"{ resource_id_prefix } Resource" ,
1056+ ParentId = GetAtt (self .logical_id , "RootResourceId" )
1057+ if parent_id_prefix is None
1058+ else GetAtt (f"{ parent_id_prefix } Resource" , "ResourceId" ),
1059+ RestApiId = self .ref ,
1060+ PathPart = r .path ,
1061+ )
1062+
1063+ result .append (resource )
1064+
1065+ # Declare the methods of this resource
1066+ for method in r .method_list :
1067+ result += self ._declare_method (
1068+ method = method ,
1069+ resource = r ,
1070+ resource_id_prefix = resource_id_prefix ,
1071+ resource_path = resource_path ,
1072+ )
1073+
1074+ # Declare the children of this resource
1075+ if r .resource_list :
1076+ result += self ._declare_resources (
1077+ resource_list = r .resource_list ,
1078+ parent_id_prefix = resource_id_prefix ,
1079+ parent_path = resource_path ,
1080+ )
1081+
1082+ return result
1083+
9641084 def _get_alias_target_attributes (self ) -> Api ._AliasTargetAttributes :
9651085 """Get atributes to pass to GetAtt for alias target."""
9661086 return {
@@ -1023,16 +1143,8 @@ def resources(self, stack: Stack) -> list[AWSObject]:
10231143
10241144 result .append (apigateway .RestApi (self .logical_id , ** api_params ))
10251145
1026- # Create an API resource
1027- resource_name = self .logical_id + "Resource"
1028- result .append (
1029- apigateway .Resource (
1030- resource_name ,
1031- ParentId = GetAtt (self .logical_id , "RootResourceId" ),
1032- RestApiId = self .ref ,
1033- PathPart = "{proxy+}" ,
1034- )
1035- )
1146+ # Create API resources and methods
1147+ result += self ._declare_resources (resource_list = self .resource_list )
10361148
10371149 # Declare the different stages
10381150 for config in self .stages_config :
@@ -1044,10 +1156,6 @@ def resources(self, stack: Stack) -> list[AWSObject]:
10441156 )
10451157 )
10461158
1047- # Declare the methods
1048- for method in self .method_list :
1049- result += self .declare_method (method = method , resource_id = Ref (resource_name ))
1050-
10511159 # Declare the domain
10521160 if self .domain_name is not None :
10531161 assert self .hosted_zone_id is not None
0 commit comments