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  }