github.com/mweagle/Sparta@v1.15.0/archetype/rest/rest.go (about) 1 package rest 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "strings" 8 9 sparta "github.com/mweagle/Sparta" 10 "github.com/pkg/errors" 11 ) 12 13 var allHTTPMethods = strings.Join([]string{ 14 http.MethodGet, 15 http.MethodHead, 16 http.MethodPost, 17 http.MethodPut, 18 http.MethodPatch, 19 http.MethodDelete, 20 http.MethodConnect, 21 http.MethodOptions, 22 http.MethodTrace, 23 }, " ") 24 25 // MethodHandlerMap is a map of http method names to their handlers 26 type MethodHandlerMap map[string]*MethodHandler 27 28 // MethodHandler represents a handler for a given HTTP method 29 type MethodHandler struct { 30 DefaultCode int 31 statusCodes []int 32 Handler interface{} 33 privileges []sparta.IAMRolePrivilege 34 options *sparta.LambdaFunctionOptions 35 headers []string 36 } 37 38 // StatusCodes is a fluent builder to append additional HTTP status codes 39 // for the given MethodHandler. It's primarily used to disamgiguate 40 // input from the NewMethodHandler constructor 41 func (mh *MethodHandler) StatusCodes(codes ...int) *MethodHandler { 42 if mh.statusCodes == nil { 43 mh.statusCodes = make([]int, 0) 44 } 45 mh.statusCodes = append(mh.statusCodes, codes...) 46 return mh 47 } 48 49 // Options is a fluent builder that allows customizing the lambda execution 50 // options for the given function 51 func (mh *MethodHandler) Options(options *sparta.LambdaFunctionOptions) *MethodHandler { 52 mh.options = options 53 return mh 54 } 55 56 // Privileges is the fluent builder to associated IAM privileges with this 57 // HTTP handler 58 func (mh *MethodHandler) Privileges(privileges ...sparta.IAMRolePrivilege) *MethodHandler { 59 if mh.privileges == nil { 60 mh.privileges = make([]sparta.IAMRolePrivilege, 0) 61 } 62 mh.privileges = append(mh.privileges, privileges...) 63 return mh 64 } 65 66 // Headers is the fluent builder that defines what headers this method returns 67 func (mh *MethodHandler) Headers(headerNames ...string) *MethodHandler { 68 if mh.headers == nil { 69 mh.headers = make([]string, 0) 70 } 71 mh.headers = append(mh.headers, headerNames...) 72 return mh 73 } 74 75 // NewMethodHandler is a constructor function to return a new MethodHandler 76 // pointer instance. 77 func NewMethodHandler(handler interface{}, defaultCode int) *MethodHandler { 78 return &MethodHandler{ 79 DefaultCode: defaultCode, 80 Handler: handler, 81 } 82 } 83 84 // ResourceDefinition represents a set of handlers for a given URL path 85 type ResourceDefinition struct { 86 URL string 87 MethodHandlers MethodHandlerMap 88 } 89 90 // Resource defines the interface an object must define in order to 91 // provide a ResourceDefinition 92 type Resource interface { 93 ResourceDefinition() (ResourceDefinition, error) 94 } 95 96 // RegisterResource creates a set of lambda handlers for the given resource 97 // and registers them with the apiGateway. The sparta Lambda handler returned 98 // slice is eligible 99 func RegisterResource(apiGateway *sparta.API, resource Resource) ([]*sparta.LambdaAWSInfo, error) { 100 101 definition, definitionErr := resource.ResourceDefinition() 102 if definitionErr != nil { 103 return nil, errors.Wrapf(definitionErr, "requesting ResourceDefinition from provider") 104 } 105 106 urlParts, urlPartsErr := url.Parse(definition.URL) 107 if urlPartsErr != nil { 108 return nil, errors.Wrapf(urlPartsErr, "parsing REST URL: %s", definition.URL) 109 } 110 // Any query params? 111 queryParams, queryParamsErr := url.ParseQuery(urlParts.RawQuery) 112 if nil != queryParamsErr { 113 return nil, errors.Wrap(queryParamsErr, "parsing REST URL query params") 114 } 115 116 // Any path params? 117 pathParams := []string{} 118 pathParts := strings.Split(urlParts.Path, "/") 119 for _, eachPathPart := range pathParts { 120 trimmedPathPart := strings.Trim(eachPathPart, "{}") 121 if trimmedPathPart != eachPathPart { 122 pathParams = append(pathParams, trimmedPathPart) 123 } 124 } 125 126 // Local function to produce a friendlyname for the provider 127 lambdaName := func(methodName string) string { 128 nameValue := fmt.Sprintf("%T_%s", resource, methodName) 129 return strings.Trim(nameValue, "_-.()*") 130 } 131 132 // Local function to handle registering the function with API Gateway 133 createAPIGEntry := func(methodName string, 134 methodHandler *MethodHandler, 135 handler *sparta.LambdaAWSInfo) error { 136 apiGWResource, apiGWResourceErr := apiGateway.NewResource(definition.URL, handler) 137 if apiGWResourceErr != nil { 138 return errors.Wrapf(apiGWResourceErr, "attempting to create API Gateway Resource") 139 } 140 statusCodes := methodHandler.statusCodes 141 if statusCodes == nil { 142 statusCodes = []int{} 143 } 144 // We only return http.StatusOK 145 apiMethod, apiMethodErr := apiGWResource.NewMethod(methodName, 146 methodHandler.DefaultCode, 147 statusCodes...) 148 if apiMethodErr != nil { 149 return apiMethodErr 150 } 151 // Do anything smart with the URL? Split the URL into components to first see 152 // if it's a URL template 153 for _, eachPathPart := range pathParams { 154 apiMethod.Parameters[fmt.Sprintf("method.request.path.%s", eachPathPart)] = true 155 } 156 157 // Then parse it to see what's up with the query param names 158 for eachQueryParam := range queryParams { 159 apiMethod.Parameters[fmt.Sprintf("method.request.querystring.%s", eachQueryParam)] = true 160 } 161 // Any headers? 162 // We used to need to whitelist these, but the header management has been moved 163 // into the VTL templating overrides and can be removed from here. 164 165 // for _, eachHeader := range methodHandler.headers { 166 // // Make this an optional header on the method response 167 // lowercaseHeaderName := strings.ToLower(eachHeader) 168 // methodHeaderKey := fmt.Sprintf("method.response.header.%s", lowercaseHeaderName) 169 170 // // for _, eachResponse := range apiMethod.Responses { 171 // // eachResponse.Parameters[methodHeaderKey] = false 172 // // } 173 // // We don't need to add the explicit mappings since it's now always 174 // // in the response mapping template. 175 176 // // Add it to the integration mappings 177 // // Then ensure every integration response knows how to pass it along... 178 // // inputSelector := fmt.Sprintf("integration.response.header.%s", eachHeader) 179 // // for _, eachIntegrationResponse := range apiMethod.Integration.Responses { 180 // // if len(eachIntegrationResponse.Parameters) <= 0 { 181 // // eachIntegrationResponse.Parameters = make(map[string]interface{}) 182 // // } 183 // // eachIntegrationResponse.Parameters[methodHeaderKey] = inputSelector 184 // // } 185 // } 186 return nil 187 } 188 resourceMap := make(map[string]*sparta.LambdaAWSInfo) 189 190 // Great, walk the map of handlers 191 for eachMethod, eachMethodDefinition := range definition.MethodHandlers { 192 if !strings.Contains(allHTTPMethods, eachMethod) { 193 return nil, errors.Errorf("unsupported HTTP method name: `%s %s`. Supported: %s", 194 eachMethod, 195 definition.URL, 196 allHTTPMethods) 197 } 198 lambdaFn, lambdaFnErr := sparta.NewAWSLambda(lambdaName(eachMethod), 199 eachMethodDefinition.Handler, 200 sparta.IAMRoleDefinition{}) 201 202 if lambdaFnErr != nil { 203 return nil, errors.Wrapf(lambdaFnErr, 204 "attempting to register url `%s %s`", eachMethod, definition.URL) 205 } 206 207 resourceMap[eachMethod] = lambdaFn 208 209 // Any options? 210 if eachMethodDefinition.options != nil { 211 lambdaFn.Options = eachMethodDefinition.options 212 } 213 214 // Any privs? 215 if len(eachMethodDefinition.privileges) != 0 { 216 lambdaFn.RoleDefinition.Privileges = eachMethodDefinition.privileges 217 } 218 219 // Register the route... 220 apiGWRegistrationErr := createAPIGEntry(eachMethod, eachMethodDefinition, lambdaFn) 221 if apiGWRegistrationErr != nil { 222 return nil, errors.Wrapf(apiGWRegistrationErr, "attemping to create resource for method: %s", http.MethodHead) 223 } 224 } 225 if len(resourceMap) <= 0 { 226 return nil, errors.Errorf("No resource methodHandlers found for resource: %T", resource) 227 } 228 // Convert this into a slice and return it... 229 lambdaResourceHandlers := make([]*sparta.LambdaAWSInfo, 230 len(resourceMap)) 231 lambdaIndex := 0 232 for _, eachLambda := range resourceMap { 233 lambdaResourceHandlers[lambdaIndex] = eachLambda 234 lambdaIndex++ 235 } 236 return lambdaResourceHandlers, nil 237 }