├── cloudwatch ├── cloudwatch_test.go ├── README.md └── cloudwatch.go ├── s3 ├── s3_test.go └── s3.go ├── ses ├── ses_test.go └── ses.go ├── sns ├── sns_test.go └── sns.go ├── dynamo ├── dynamo_test.go └── dynamo.go ├── cognito ├── cognito_test.go └── cognito.go ├── kinesis ├── kinesis_test.go └── kinesis.go ├── example_test.go ├── _examples ├── s3 │ ├── README.md │ ├── main.go │ └── event.json ├── proxy │ ├── main.go │ └── event.json ├── kinesis │ ├── event.json │ ├── main.go │ └── README.md └── dynamo │ ├── main.go │ ├── README.md │ └── event.json ├── slack ├── README.md └── slack.go ├── apiai ├── README.md ├── apiai_test.go └── apiai.go ├── LICENSE ├── proxy ├── proxy.go ├── decoder.go ├── request.go ├── README.md ├── responsewriter.go └── event.go ├── apex_test.go ├── logs ├── logs_test.go └── logs.go ├── cloudformation ├── handler_test.go └── handler.go ├── Readme.md └── apex.go /cloudwatch/cloudwatch_test.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | -------------------------------------------------------------------------------- /s3/s3_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // HandlerFunc apex.Handler assertion. 11 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 12 | return nil 13 | }) 14 | 15 | func Test(t *testing.T) { 16 | assert.Nil(t, nil) 17 | // TODO: unmarshalling test 18 | } 19 | -------------------------------------------------------------------------------- /ses/ses_test.go: -------------------------------------------------------------------------------- 1 | package ses 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // HandlerFunc apex.Handler assertion. 11 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 12 | return nil 13 | }) 14 | 15 | func Test(t *testing.T) { 16 | assert.Nil(t, nil) 17 | // TODO: unmarshalling test 18 | } 19 | -------------------------------------------------------------------------------- /sns/sns_test.go: -------------------------------------------------------------------------------- 1 | package sns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // HandlerFunc apex.Handler assertion. 11 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 12 | return nil 13 | }) 14 | 15 | func Test(t *testing.T) { 16 | assert.Nil(t, nil) 17 | // TODO: unmarshalling test 18 | } 19 | -------------------------------------------------------------------------------- /dynamo/dynamo_test.go: -------------------------------------------------------------------------------- 1 | package dynamo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // HandlerFunc apex.Handler assertion. 11 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 12 | return nil 13 | }) 14 | 15 | func Test(t *testing.T) { 16 | assert.Nil(t, nil) 17 | // TODO: unmarshalling test 18 | } 19 | -------------------------------------------------------------------------------- /cognito/cognito_test.go: -------------------------------------------------------------------------------- 1 | package cognito 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // HandlerFunc apex.Handler assertion. 11 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 12 | return nil 13 | }) 14 | 15 | func Test(t *testing.T) { 16 | assert.Nil(t, nil) 17 | // TODO: unmarshalling test 18 | } 19 | -------------------------------------------------------------------------------- /kinesis/kinesis_test.go: -------------------------------------------------------------------------------- 1 | package kinesis 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // HandlerFunc apex.Handler assertion. 11 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 12 | return nil 13 | }) 14 | 15 | func Test(t *testing.T) { 16 | assert.Nil(t, nil) 17 | // TODO: unmarshalling test 18 | } 19 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package apex_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/apex/go-apex" 7 | ) 8 | 9 | type Message struct { 10 | Hello string `json:"hello"` 11 | } 12 | 13 | // Example of a Lambda function handling arbitrary JSON input. 14 | func Example() { 15 | apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) { 16 | return &Message{"world"}, nil 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /_examples/s3/README.md: -------------------------------------------------------------------------------- 1 | # S3 Example 2 | 3 | The example program receives a S3 event and prints a specific object's value to stdout. 4 | 5 | ## Setup 6 | 7 | Edit event.json file to reference your real S3 object. (awsRegion, bucket.name & object.key) 8 | Run the program locally: 9 | 10 | ``` 11 | go run main.go < event.json 12 | ``` 13 | 14 | ## Read S3 Object 15 | 16 | In the example the [AWS Go SDK](http://aws.amazon.com/sdk-for-go/) is used to read the S3 Object. 17 | -------------------------------------------------------------------------------- /_examples/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/apex/go-apex" 8 | "github.com/apex/go-apex/proxy" 9 | ) 10 | 11 | func main() { 12 | mux := http.NewServeMux() 13 | mux.HandleFunc("/", handleRoot) 14 | mux.HandleFunc("/hello/", handleHello) 15 | 16 | apex.Handle(proxy.Serve(mux)) 17 | } 18 | 19 | func handleRoot(w http.ResponseWriter, r *http.Request) { 20 | w.Write([]byte("Root")) 21 | } 22 | 23 | func handleHello(w http.ResponseWriter, r *http.Request) { 24 | _, name := path.Split(r.URL.Path) 25 | w.Write([]byte("Hello " + name)) 26 | } 27 | -------------------------------------------------------------------------------- /_examples/kinesis/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "Records": [ 4 | { 5 | "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", 6 | "eventVersion": "1.0", 7 | "Kinesis": { 8 | "partitionKey": "partitionKey-3", 9 | "data": "eyJtZXNzYWdlIjogIkhlbGxvIGZyb20gQXBleCEifQ==", 10 | "kinesisSchemaVersion": "1.0", 11 | "sequenceNumber": "49545115243490985018280067714973144582180062593244200961" 12 | }, 13 | "invokeIdentityArn": "arn:aws:iam::EXAMPLE", 14 | "eventName": "aws:kinesis:record", 15 | "eventSourceARN": "arn:aws:kinesis:EXAMPLE", 16 | "eventSource": "aws:kinesis", 17 | "awsRegion": "us-east-1" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cloudwatch/README.md: -------------------------------------------------------------------------------- 1 | # Apex CloudWatch 2 | 3 | Providing CloudWatch Events for [Apex](https://github.com/apex/go-apex) 4 | 5 | ## Features 6 | Currently only supports the following event details: 7 | - AutoScaling 8 | - EC2 9 | - Console Sign-In 10 | - Schedule 11 | 12 | Unknown events will have empty details 13 | 14 | ## Example 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "log" 21 | 22 | "github.com/apex/go-apex" 23 | "github.com/apex/go-apex/cloudwatch" 24 | ) 25 | 26 | func main() { 27 | cloudwatch.HandleFunc(func(evt *cloudwatch.Event, ctx *apex.Context) error { 28 | log.Println("Handler called") 29 | return nil 30 | } 31 | } 32 | ``` 33 | 34 | --- 35 | 36 | GitHub [@sthulb](https://github.com/sthulb)  ·  37 | Twitter [@sthulb](https://twitter.com/sthulb) 38 | -------------------------------------------------------------------------------- /_examples/dynamo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/apex/go-apex" 7 | "github.com/apex/go-apex/dynamo" 8 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 9 | ) 10 | 11 | type newImage struct { 12 | ExampleKey string 13 | } 14 | 15 | func main() { 16 | dynamo.HandleFunc(func(event *dynamo.Event, ctx *apex.Context) error { 17 | // Iterate all event records 18 | for _, record := range event.Records { 19 | 20 | // only act on INSERTs 21 | if record.EventName == "INSERT" { 22 | 23 | n := newImage{} 24 | // Unmarshal the data for NewImage 25 | dynamodbattribute.UnmarshalMap(record.Dynamodb.NewImage, &n) 26 | 27 | // Print the example attribute. (Don't do this in your function!) 28 | fmt.Println(n.ExampleKey) 29 | } 30 | } 31 | 32 | return nil 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /slack/README.md: -------------------------------------------------------------------------------- 1 | # Apex Slack 2 | 3 | Providing Slack Events for [Apex](https://github.com/apex/go-apex) 4 | 5 | ## Features 6 | This is intended for use with API Gateway or services that use JSON. 7 | 8 | ## Example 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | apex "github.com/apex/go-apex" 15 | "github.com/apex/go-apex/slack" 16 | ) 17 | 18 | func main() { 19 | slack.HandleFunc(func(event *slack.Event, ctx *apex.Context) (interface{}, error) { 20 | // useEventData(event) 21 | 22 | // Construct response message 23 | var message slack.ResponseMessage 24 | message.ResponseType = "in_channel" 25 | message.Text = "response text" 26 | 27 | return message, nil 28 | }) 29 | } 30 | ``` 31 | 32 | --- 33 | 34 | GitHub [@DaveBlooman](https://github.com/DaveBlooman)  ·  35 | Twitter [@dblooman](https://twitter.com/dblooman) 36 | -------------------------------------------------------------------------------- /_examples/dynamo/README.md: -------------------------------------------------------------------------------- 1 | # DynamoDB Example 2 | 3 | The example program receives a DynamoDB event and prints a specific attribute's value to stdout. 4 | 5 | ## Setup 6 | 7 | Run the program locally: 8 | 9 | ``` 10 | go run main.go < event.json 11 | ``` 12 | 13 | ## Unmarshaling DynamoDB Records 14 | 15 | DynamoDB uses a somewhat awkward notation to represent Attributes and Values in the event. It's a straight representation of the DynamoDB JSON which needs to be parsed to be useful. 16 | 17 | In the example the [AWS Go SDK](http://aws.amazon.com/sdk-for-go/) is used to Unmarshal the Dynamo Attributes. These are stored in the struct `newImage`. 18 | 19 | Depending on the event settings you could also Unmarshal `record.Dynamodb.OldImage` as needed. 20 | 21 | This method of Unmarshalling is optional you may elect to handle `record.Dynamodb.NewImage` (etc) in another way. 22 | -------------------------------------------------------------------------------- /_examples/s3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | apex "github.com/apex/go-apex" 8 | apexS3 "github.com/apex/go-apex/s3" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/s3" 12 | ) 13 | 14 | func main() { 15 | apexS3.HandleFunc(func(event *apexS3.Event, ctx *apex.Context) error { 16 | for _, record := range event.Records { 17 | svc := s3.New(session.New(&aws.Config{Region: aws.String(record.AWSRegion)})) 18 | out, err := svc.GetObject(&s3.GetObjectInput{ 19 | Bucket: aws.String(record.S3.Bucket.Name), 20 | Key: aws.String(record.S3.Object.Key), 21 | }) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | bytes, err := ioutil.ReadAll(out.Body) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | log.Print(string(bytes)) 30 | } 31 | return nil 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /apiai/README.md: -------------------------------------------------------------------------------- 1 | # Apex Api.ai 2 | 3 | Providing [Api.ai Webhook Events](https://docs.api.ai/docs/webhook) for [Apex](https://github.com/apex/go-apex) 4 | 5 | ## Features 6 | This is intended for use with API Gateway or services that use JSON. 7 | 8 | ## Example 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | apex "github.com/apex/go-apex" 15 | "github.com/apex/go-apex/apiai" 16 | ) 17 | 18 | func main() { 19 | apiai.HandleFunc(func(event *apiai.Event, ctx *apex.Context) (interface{}, error) { 20 | 21 | // Construct response message 22 | var message apiai.ResponseMessage 23 | message.Speech = "Hello!" 24 | message.DisplayText = "Hey, nice to meet you!" 25 | 26 | return message, nil 27 | }) 28 | } 29 | ``` 30 | 31 | --- 32 | 33 | GitHub [@edoardo849](https://github.com/edoardo849)  ·  34 | Twitter [@edoardo849](https://twitter.com/edoardo849)  ·  35 | [Medium](https://medium.com/@edoardo849) 36 | -------------------------------------------------------------------------------- /_examples/s3/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "Records": [ 4 | { 5 | "eventVersion": "2.0", 6 | "eventSource": "aws:s3", 7 | "awsRegion": "us-east-1", 8 | "eventTime": "2016-04-01T12:34:56+00:00", 9 | "eventName": "ObjectCreated:Put", 10 | "userIdentity": { 11 | "principalId": "AWS:AIXXXXXXXXXXXXXXXXXXX" 12 | }, 13 | "requestParameters": { 14 | "sourceIPAddress": "127.0.0.1" 15 | }, 16 | "S3": { 17 | "s3SchemaVersion": "1.0", 18 | "configurationId": "s3-events-notifications-name", 19 | "bucket": { 20 | "name": "s3-bucket-name", 21 | "ownerIdentity": { 22 | "principalId": "AXXXXXXXXXXXXX" 23 | }, 24 | "arn": "arn:aws:s3:::s3-bucket-name" 25 | }, 26 | "object": { 27 | "key": "s3-object-name", 28 | "size": 945, 29 | "eTag": "etag0123456789", 30 | "versionId": "", 31 | "sequencer": "000123456" 32 | } 33 | } 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/apex/go-apex" 9 | ) 10 | 11 | // Serve adaptes an http.Handler to the apex.Handler interface. 12 | func Serve(h http.Handler) apex.Handler { 13 | if h == nil { 14 | h = http.DefaultServeMux 15 | } 16 | 17 | return &handler{h} 18 | } 19 | 20 | // Handler implements the apex.Handler interface and adapts it to an 21 | // http.Handler by converting the incoming event to an http.Request object 22 | type handler struct { 23 | Handler http.Handler 24 | } 25 | 26 | // Handle accepts a request from the apex shim and dispatches it to an http.Handler 27 | func (p *handler) Handle(event json.RawMessage, ctx *apex.Context) (interface{}, error) { 28 | proxyEvent := &Event{} 29 | 30 | err := json.Unmarshal(event, proxyEvent) 31 | if err != nil { 32 | return nil, fmt.Errorf("Parse proxy event: %s", err) 33 | } 34 | 35 | req, err := buildRequest(proxyEvent, ctx) 36 | if err != nil { 37 | return nil, fmt.Errorf("Build request: %s", err) 38 | } 39 | 40 | res := &ResponseWriter{} 41 | p.Handler.ServeHTTP(res, req) 42 | res.finish() 43 | 44 | return &res.response, nil 45 | } 46 | -------------------------------------------------------------------------------- /_examples/proxy/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "resource": "/{proxy+}", 4 | "path": "/hello/world", 5 | "httpMethod": "GET", 6 | "headers": null, 7 | "queryStringParameters": null, 8 | "pathParameters": { 9 | "proxy": "hello/world" 10 | }, 11 | "stageVariables": null, 12 | "requestContext": { 13 | "accountId": "EXAMPLE", 14 | "resourceId": "EXAMPLE", 15 | "stage": "test-invoke-stage", 16 | "requestId": "test-invoke-request", 17 | "identity": { 18 | "cognitoIdentityPoolId": null, 19 | "accountId": "EXAMPLE", 20 | "cognitoIdentityId": null, 21 | "caller": "EXAMPLE", 22 | "apiKey": "test-invoke-api-key", 23 | "sourceIp": "test-invoke-source-ip", 24 | "accessKey": "EXAMPLE", 25 | "cognitoAuthenticationType": null, 26 | "cognitoAuthenticationProvider": null, 27 | "userArn": "arn:aws:iam::EXAMPLE:root", 28 | "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", 29 | "user": "EXAMPLE" 30 | }, 31 | "resourcePath": "/{proxy+}", 32 | "httpMethod": "GET", 33 | "apiId": "EXAMPLE" 34 | }, 35 | "body": null, 36 | "isBase64Encoded": false 37 | } 38 | } -------------------------------------------------------------------------------- /_examples/kinesis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/apex/go-apex" 9 | "github.com/apex/go-apex/kinesis" 10 | ) 11 | 12 | const requestBinURL = "http://requestb.in/14cd4uz1" 13 | 14 | type data struct { 15 | Message string `json:"message"` 16 | } 17 | 18 | func main() { 19 | kinesis.HandleFunc(func(event *kinesis.Event, ctx *apex.Context) error { 20 | var d data 21 | 22 | for _, record := range event.Records { 23 | // Unmarshal the message data 24 | err := json.Unmarshal(record.Kinesis.Data, &d) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // New request 30 | req, err := http.NewRequest("GET", requestBinURL, nil) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Query params 36 | c := &http.Client{} 37 | q := req.URL.Query() 38 | q.Set("message", d.Message) 39 | req.URL.RawQuery = q.Encode() 40 | 41 | // Make request 42 | resp, err := c.Do(req) 43 | if err != nil { 44 | return err 45 | } 46 | defer resp.Body.Close() 47 | 48 | // Check response 49 | if resp.StatusCode != http.StatusOK { 50 | return fmt.Errorf("%v", resp) 51 | } 52 | } 53 | 54 | return nil 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /_examples/dynamo/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": { 3 | "Records": [ 4 | { 5 | "eventID": "1", 6 | "eventVersion": "1.0", 7 | "dynamodb": { 8 | "Keys": { 9 | "ExampleKey": { 10 | "S": "foo" 11 | } 12 | }, 13 | "NewImage": { 14 | "ExampleKey": { 15 | "S": "bar" 16 | } 17 | }, 18 | "StreamViewType": "NEW_AND_OLD_IMAGES", 19 | "SequenceNumber": "111", 20 | "SizeBytes": 26 21 | }, 22 | "awsRegion": "us-west-2", 23 | "eventName": "INSERT", 24 | "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", 25 | "eventSource": "aws:dynamodb" 26 | }, 27 | { 28 | "eventID": "2", 29 | "eventVersion": "1.0", 30 | "dynamodb": { 31 | "SequenceNumber": "222", 32 | "Keys": { 33 | "ExampleKey": { 34 | "S": "fix" 35 | } 36 | }, 37 | "SizeBytes": 59, 38 | "NewImage": { 39 | "ExampleKey": { 40 | "S": "qix" 41 | } 42 | }, 43 | "StreamViewType": "NEW_AND_OLD_IMAGES" 44 | }, 45 | "awsRegion": "us-west-2", 46 | "eventName": "INSERT", 47 | "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", 48 | "eventSource": "aws:dynamodb" 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apex_test.go: -------------------------------------------------------------------------------- 1 | package apex 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/tj/assert" 11 | ) 12 | 13 | var eventInput = `{ 14 | "event": { 15 | "foo": "bar" 16 | }, 17 | "context": { 18 | "invokeid": "test" 19 | } 20 | }` 21 | 22 | func TestHandler(t *testing.T) { 23 | var wg sync.WaitGroup 24 | 25 | n := 50 26 | wg.Add(n) 27 | 28 | var events []string 29 | var contexts []string 30 | h := HandlerFunc(func(event json.RawMessage, ctx *Context) (interface{}, error) { 31 | events = append(events, string(event)) 32 | contexts = append(contexts, ctx.InvokeID) 33 | wg.Done() 34 | return nil, nil 35 | }) 36 | 37 | var buf bytes.Buffer 38 | pr, pw := io.Pipe() 39 | 40 | m := &manager{ 41 | Reader: pr, 42 | Writer: &buf, 43 | Handler: h, 44 | } 45 | 46 | go m.Start() 47 | 48 | go func() { 49 | for i := 0; i < n; i++ { 50 | pw.Write([]byte(eventInput)) 51 | } 52 | pw.Close() 53 | }() 54 | 55 | wg.Wait() 56 | 57 | for i, e := range events { 58 | assert.Equal(t, "{\n\t\t\"foo\": \"bar\"\n\t}", e) 59 | assert.Equal(t, "test", contexts[i]) 60 | } 61 | } 62 | 63 | func BenchmarkHandler(b *testing.B) { 64 | h := HandlerFunc(func(event json.RawMessage, ctx *Context) (interface{}, error) { 65 | return nil, nil 66 | }) 67 | 68 | var buf bytes.Buffer 69 | pr, pw := io.Pipe() 70 | 71 | m := &manager{ 72 | Reader: pr, 73 | Writer: &buf, 74 | Handler: h, 75 | } 76 | 77 | go m.Start() 78 | 79 | for i := 0; i < b.N; i++ { 80 | pw.Write([]byte(eventInput)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/apex/go-apex" 7 | ) 8 | 9 | // Event data JSON 10 | type Event struct { 11 | ChannelID string `json:"channel_id"` 12 | ChannelName string `json:"channel_name"` 13 | Command string `json:"command"` 14 | ResponseURL string `json:"response_url"` 15 | TeamDomain string `json:"team_domain"` 16 | TeamID string `json:"team_id"` 17 | Text string `json:"text"` 18 | Token string `json:"token"` 19 | UserID string `json:"user_id"` 20 | UserName string `json:"user_name"` 21 | } 22 | 23 | // ResponseMessage JSON 24 | type ResponseMessage struct { 25 | ResponseType string `json:"response_type"` 26 | Text string `json:"text"` 27 | } 28 | 29 | // Handler handles Slack Events 30 | type Handler interface { 31 | HandleSlackEvent(*Event, *apex.Context) (interface{}, error) 32 | } 33 | 34 | // HandlerFunc unmarshals Slack Events before passing control. 35 | type HandlerFunc func(*Event, *apex.Context) (interface{}, error) 36 | 37 | // Handle implements apex.Handler. 38 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 39 | var event Event 40 | 41 | if err := json.Unmarshal(data, &event); err != nil { 42 | return nil, err 43 | } 44 | 45 | return h(&event, ctx) 46 | } 47 | 48 | // HandleFunc handles Slack Events with callback function. 49 | func HandleFunc(h HandlerFunc) { 50 | apex.Handle(h) 51 | } 52 | 53 | // Handle Slack Events with handler. 54 | func Handle(h Handler) { 55 | HandleFunc(HandlerFunc(h.HandleSlackEvent)) 56 | } 57 | -------------------------------------------------------------------------------- /logs/logs_test.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/apex/go-apex" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // HandlerFunc apex.Handler assertion. 12 | var _ apex.Handler = HandlerFunc(func(event *Event, ctx *apex.Context) error { 13 | return nil 14 | }) 15 | 16 | func fixture(path string) []byte { 17 | b, err := ioutil.ReadFile(path) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | return b 23 | } 24 | 25 | var event = []byte(` 26 | { 27 | "awslogs": { 28 | "data": "H4sIAAAAAAAA/zWPwQ6CMBBEf4U0HknaIgJ6IxG56AluhpgCKzYBStpFYoz/botxT5uZt5PZNxnAGNFB+ZqAHDxyTMv0dsmKIs0z4hO1jKCdzoNtuIviZM+s2qsu12qenEHFYmgvhroVtOnV3C4CmweCwR9YoAYxODJgPKIsoUFIr5tzWmZFWYm6WZMta+baNFpOKNV4kj2CNvbqSu6/nZNqzcueMOLqvIls12pubABK+wuKwdXiYRywhHEexfut/3/S0a6ZHDsPH+D95U/1+QKw9SHdCQEAAA==" 29 | } 30 | } 31 | `) 32 | 33 | func TestHandlerFunc_Handle(t *testing.T) { 34 | called := false 35 | 36 | fn := func(e *Event, c *apex.Context) error { 37 | called = true 38 | assert.Equal(t, 1, len(e.LogEvents)) 39 | 40 | assert.Equal(t, "DATA_MESSAGE", e.MessageType) 41 | assert.Equal(t, "1234567890", e.Owner) 42 | assert.Equal(t, "/aws/lambda/cloudwatchtest", e.LogGroup) 43 | assert.Equal(t, "2016/08/24/[$LATEST]abc12345", e.LogStream) 44 | assert.Equal(t, 1, len(e.SubscriptionFilters)) 45 | assert.Equal(t, "filters1", e.SubscriptionFilters[0]) 46 | 47 | record := e.LogEvents[0] 48 | assert.Equal(t, "11111", record.ID) 49 | assert.Equal(t, "testing the message", record.Message) 50 | 51 | return nil 52 | } 53 | 54 | _, err := HandlerFunc(fn).Handle(event, nil) 55 | assert.NoError(t, err) 56 | 57 | assert.True(t, called, "function never called") 58 | } 59 | -------------------------------------------------------------------------------- /_examples/kinesis/README.md: -------------------------------------------------------------------------------- 1 | # Kinesis Example 2 | 3 | The example program receives a Kinesis event with an array of records and forwards the `message` from each record to a [RequestBin](http://requestb.in/) endpoint. 4 | 5 | ## Setup 6 | 7 | Run the program locally: 8 | 9 | ``` 10 | go run main.go < event.json 11 | ``` 12 | 13 | View the the message in the RequestBin bucket: 14 | 15 | request_bin 16 | 17 | ### Base64 Encoding 18 | 19 | The record data on the Kinesis stream is Base64 encoded. Therefore in order to emulate a live message from Kinesis we'll manually encode the JSON payload. 20 | 21 | Example JSON payload: 22 | 23 | ``` 24 | {"message": "Hello from Apex!"} 25 | ``` 26 | 27 | After Base64 encoding: 28 | 29 | ``` 30 | eyJtZXNzYWdlIjogIkhlbGxvIGZyb20gQXBleCEifQ== 31 | ``` 32 | 33 | The encoded data withing the `event.json` payload: 34 | 35 | ```json 36 | { 37 | "event": { 38 | "Records": [ 39 | { 40 | "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", 41 | "eventVersion": "1.0", 42 | "Kinesis": { 43 | "partitionKey": "partitionKey-3", 44 | "data": "eyJtZXNzYWdlIjogIkhlbGxvIGZyb20gQXBleCEifQ==", // Base64 encoded data 45 | "kinesisSchemaVersion": "1.0", 46 | "sequenceNumber": "49545115243490985018280067714973144582180062593244200961" 47 | }, 48 | "invokeIdentityArn": "arn:aws:iam::EXAMPLE", 49 | "eventName": "aws:kinesis:record", 50 | "eventSourceARN": "arn:aws:kinesis:EXAMPLE", 51 | "eventSource": "aws:kinesis", 52 | "awsRegion": "us-east-1" 53 | } 54 | ] 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /cognito/cognito.go: -------------------------------------------------------------------------------- 1 | // Package cognito provides structs for working with AWS Cognito records. 2 | package cognito 3 | 4 | import ( 5 | "encoding/json" 6 | 7 | "github.com/apex/go-apex" 8 | ) 9 | 10 | // Event represents a Cognito event with one or more records. 11 | type Event struct { 12 | Records []*Record `json:"Records"` 13 | } 14 | 15 | // Change represents a single Cognito data record change. 16 | type Change struct { 17 | Old string `json:"oldValue"` 18 | New string `json:"newValue"` 19 | Op string `json:"op"` 20 | } 21 | 22 | // Record represents a single Cognito record. 23 | type Record struct { 24 | Version int `json:"version"` 25 | EventType string `json:"eventType"` 26 | Region string `json:"region"` 27 | IdentityPoolID string `json:"identityPoolId"` 28 | IdentityID string `json:"identityId"` 29 | DatasetName string `json:"datasetName"` 30 | DatasetRecords map[string]*Change `json:"datasetRecords"` 31 | } 32 | 33 | // Handler handles Cognito events. 34 | type Handler interface { 35 | HandleCognito(*Event, *apex.Context) error 36 | } 37 | 38 | // HandlerFunc unmarshals Cognito events before passing control. 39 | type HandlerFunc func(*Event, *apex.Context) error 40 | 41 | // Handle implements apex.Handler. 42 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 43 | var event Event 44 | 45 | if err := json.Unmarshal(data, &event); err != nil { 46 | return nil, err 47 | } 48 | 49 | if err := h(&event, ctx); err != nil { 50 | return nil, err 51 | } 52 | 53 | return event, nil 54 | } 55 | 56 | // HandleFunc handles Cognito events with callback function. 57 | func HandleFunc(h HandlerFunc) { 58 | apex.Handle(h) 59 | } 60 | 61 | // Handle Cognito events with handler. 62 | func Handle(h Handler) { 63 | HandleFunc(HandlerFunc(h.HandleCognito)) 64 | } 65 | -------------------------------------------------------------------------------- /kinesis/kinesis.go: -------------------------------------------------------------------------------- 1 | // Package kinesis provides structs for working with AWS Kinesis records. 2 | package kinesis 3 | 4 | import ( 5 | "encoding/json" 6 | 7 | "github.com/apex/go-apex" 8 | ) 9 | 10 | // Event represents a Kinesis event with one or more records. 11 | type Event struct { 12 | Records []*Record `json:"Records"` 13 | } 14 | 15 | // Record represents a single Kinesis record. 16 | type Record struct { 17 | EventSource string `json:"eventSource"` 18 | EventVersion string `json:"eventVersion"` 19 | EventID string `json:"eventID"` 20 | EventName string `json:"eventName"` 21 | InvokeIdentityARN string `json:"invokeIdentityArn"` 22 | AWSRegion string `json:"awsRegion"` 23 | EventSourceARN string `json:"eventSourceARN"` 24 | Kinesis struct { 25 | SchemaVersion string `json:"kinesisSchemaVersion"` 26 | PartitionKey string `json:"partitionKey"` 27 | SequenceNumber string `json:"sequenceNumber"` 28 | Data []byte `json:"data"` 29 | } 30 | } 31 | 32 | // Handler handles Kinesis events. 33 | type Handler interface { 34 | HandleKinesis(*Event, *apex.Context) error 35 | } 36 | 37 | // HandlerFunc unmarshals Kinesis events before passing control. 38 | type HandlerFunc func(*Event, *apex.Context) error 39 | 40 | func (h HandlerFunc) HandleKinesis(event *Event, ctx *apex.Context) error { 41 | return h(event, ctx) 42 | } 43 | 44 | // Handle implements apex.Handler. 45 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 46 | var event Event 47 | 48 | if err := json.Unmarshal(data, &event); err != nil { 49 | return nil, err 50 | } 51 | 52 | return nil, h(&event, ctx) 53 | } 54 | 55 | // HandleFunc handles Kinesis events with callback function. 56 | func HandleFunc(h HandlerFunc) { 57 | apex.Handle(h) 58 | } 59 | 60 | // Handle Kinesis events with handler. 61 | func Handle(h Handler) { 62 | HandleFunc(HandlerFunc(h.HandleKinesis)) 63 | } 64 | -------------------------------------------------------------------------------- /cloudformation/handler_test.go: -------------------------------------------------------------------------------- 1 | package cloudformation 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/apex/go-apex" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // HandlerFunc apex.Handler assertion. 14 | var _ apex.Handler = HandlerFunc(func(req *Request, ctx *apex.Context) (interface{}, error) { 15 | return nil, nil 16 | }) 17 | 18 | var sampleRequest = Request{ 19 | RequestType: "Create", 20 | ResponseURL: "http://pre-signed-S3-url-for-response", 21 | StackID: "arn:aws:cloudformation:us-west-2:EXAMPLE/stack-name/guid", 22 | RequestID: "unique id for this create request", 23 | ResourceType: "Custom::TestResource", 24 | LogicalResourceID: "MyTestResource", 25 | ResourceProperties: map[string]interface{}{ 26 | "Name": "Value", 27 | "List": []string{"1", "2", "3"}, 28 | }, 29 | } 30 | 31 | func TestResponseToResponseURL(t *testing.T) { 32 | responses := []Response{} 33 | 34 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | defer r.Body.Close() 36 | 37 | var resp Response 38 | if err := json.NewDecoder(r.Body).Decode(&resp); err != nil { 39 | http.Error(w, err.Error(), http.StatusInternalServerError) 40 | } 41 | 42 | responses = append(responses, resp) 43 | })) 44 | defer ts.Close() 45 | 46 | h := HandlerFunc(func(r *Request, ctx *apex.Context) (interface{}, error) { 47 | return map[string]interface{}{"TestData": "Yes"}, nil 48 | }) 49 | 50 | r := sampleRequest 51 | r.ResponseURL = ts.URL 52 | 53 | b, err := json.Marshal(r) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | _, err = h.Handle(json.RawMessage(b), &apex.Context{}) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | assert.Len(t, responses, 1, "Cloudformation server expected a response") 64 | assert.Equal(t, responses[0].Status, "SUCCESS") 65 | assert.Equal(t, responses[0].Data.(map[string]interface{})["TestData"], "Yes") 66 | } 67 | -------------------------------------------------------------------------------- /sns/sns.go: -------------------------------------------------------------------------------- 1 | // Package sns provides structs for working with AWS SNS records. 2 | package sns 3 | 4 | import ( 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/apex/go-apex" 9 | ) 10 | 11 | // Event represents a SNS event. It is safe to assume a single 12 | // record will be present, as AWS will not send more than one. 13 | type Event struct { 14 | Records []*Record `json:"Records"` 15 | } 16 | 17 | // Record represents a single SNS record. 18 | type Record struct { 19 | EventSource string `json:"EventSource"` 20 | EventVersion string `json:"EventVersion"` 21 | SNS struct { 22 | Type string `json:"Type"` 23 | MessageID string `json:"MessageID"` 24 | TopicARN string `json:"TopicArn"` 25 | Subject string `json:"Subject"` 26 | Message string `json:"Message"` 27 | Timestamp time.Time `json:"Timestamp"` 28 | SignatureVersion string `json:"SignatureVersion"` 29 | Signature string `json:"Signature"` 30 | SignatureCertURL string `json:"SignatureCertURL"` 31 | UnsubscribeURL string `json:"UnsubscribeURL"` 32 | MessageAttributes map[string]interface{} `json:"MessageAttributes"` 33 | } `json:"Sns"` 34 | } 35 | 36 | // Handler handles SNS events. 37 | type Handler interface { 38 | HandleSNS(*Event, *apex.Context) error 39 | } 40 | 41 | // HandlerFunc unmarshals SNS events before passing control. 42 | type HandlerFunc func(*Event, *apex.Context) error 43 | 44 | // Handle implements apex.Handler. 45 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 46 | var event Event 47 | 48 | if err := json.Unmarshal(data, &event); err != nil { 49 | return nil, err 50 | } 51 | 52 | return nil, h(&event, ctx) 53 | } 54 | 55 | // HandleFunc handles SNS events with callback function. 56 | func HandleFunc(h HandlerFunc) { 57 | apex.Handle(h) 58 | } 59 | 60 | // Handle SNS events with handler. 61 | func Handle(h Handler) { 62 | HandleFunc(HandlerFunc(h.HandleSNS)) 63 | } 64 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Apex Golang 3 | 4 | Golang runtime support for Apex/Lambda – providing handlers for Lambda sources, and runtime requirements such as implementing the Node.js shim stdio interface. 5 | 6 | __NOTE__: `apex` v1.x supports native Go, so you should use https://github.com/aws/aws-lambda-go instead of this package. 7 | 8 | ## Features 9 | 10 | Currently supports: 11 | 12 | - Node.js shim 13 | - Environment variable population 14 | - Arbitrary JSON 15 | - CloudWatch Logs 16 | - Cognito 17 | - Kinesis 18 | - Dynamo 19 | - S3 20 | - SNS 21 | - SES 22 | 23 | ## Example 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "encoding/json" 30 | "strings" 31 | 32 | "github.com/apex/go-apex" 33 | ) 34 | 35 | type message struct { 36 | Value string `json:"value"` 37 | } 38 | 39 | func main() { 40 | apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) { 41 | var m message 42 | 43 | if err := json.Unmarshal(event, &m); err != nil { 44 | return nil, err 45 | } 46 | 47 | m.Value = strings.ToUpper(m.Value) 48 | 49 | return m, nil 50 | }) 51 | } 52 | ``` 53 | 54 | Run the program: 55 | 56 | ``` 57 | echo '{"event":{"value":"Hello World!"}}' | go run main.go 58 | {"value":{"value":"HELLO WORLD!"}} 59 | ``` 60 | 61 | ## Notes 62 | 63 | Due to the Node.js [shim](http://apex.run/#understanding-the-shim) required to run Go in Lambda, you __must__ use stderr for logging – stdout is reserved for the shim. 64 | 65 | ## Badges 66 | 67 | [![Build Status](https://semaphoreci.com/api/v1/projects/66c27cb2-5e00-469e-bfa0-b577cac48053/675168/badge.svg)](https://semaphoreci.com/tj/go-apex) 68 | [![GoDoc](https://godoc.org/github.com/apex/go-apex?status.svg)](https://godoc.org/github.com/apex/go-apex) 69 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 70 | ![](https://img.shields.io/badge/status-stable-green.svg) 71 | [![](http://apex.sh/images/badge.svg)](https://apex.sh/ping/) 72 | 73 | --- 74 | 75 | > [tjholowaychuk.com](http://tjholowaychuk.com)  ·  76 | > GitHub [@tj](https://github.com/tj)  ·  77 | > Twitter [@tjholowaychuk](https://twitter.com/tjholowaychuk) 78 | -------------------------------------------------------------------------------- /dynamo/dynamo.go: -------------------------------------------------------------------------------- 1 | // Package dynamo provides structs for working with AWS Dynamo records. 2 | package dynamo 3 | 4 | import ( 5 | "encoding/json" 6 | 7 | "github.com/apex/go-apex" 8 | "github.com/aws/aws-sdk-go/service/dynamodb" 9 | ) 10 | 11 | // Event represents a Dynamo event with one or more records. 12 | type Event struct { 13 | Records []*Record `json:"Records"` 14 | } 15 | 16 | // Record represents a single Dynamo record. 17 | type Record struct { 18 | EventID string `json:"eventID"` 19 | EventName string `json:"eventName"` 20 | EventSource string `json:"eventSource"` 21 | EventSourceARN string `json:"eventSourceARN"` 22 | EventVersion string `json:"eventVersion"` 23 | AWSRegion string `json:"awsRegion"` 24 | Dynamodb *StreamRecord `json:"dynamodb"` 25 | UserIdentity UserIdentity `json:"userIdentity,omitempty"` 26 | } 27 | 28 | type UserIdentity struct { 29 | Type string `json:"type,omitempty"` 30 | PrincipleID string `json:"principalId,omitempty"` 31 | } 32 | 33 | // StreamRecord represents a Dynamo stream records 34 | type StreamRecord struct { 35 | ApproximateCreationDateTime int64 36 | Keys map[string]*dynamodb.AttributeValue 37 | NewImage map[string]*dynamodb.AttributeValue 38 | OldImage map[string]*dynamodb.AttributeValue 39 | SequenceNumber string 40 | SizeBytes int64 41 | StreamViewType string 42 | } 43 | 44 | // Handler handles Dynamo events. 45 | type Handler interface { 46 | HandleDynamo(*Event, *apex.Context) error 47 | } 48 | 49 | // HandlerFunc unmarshals Dynamo events before passing control. 50 | type HandlerFunc func(*Event, *apex.Context) error 51 | 52 | // Handle implements apex.Handler. 53 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 54 | var event Event 55 | 56 | if err := json.Unmarshal(data, &event); err != nil { 57 | return nil, err 58 | } 59 | 60 | return nil, h(&event, ctx) 61 | } 62 | 63 | // HandleFunc handles Dynamo events with callback function. 64 | func HandleFunc(h HandlerFunc) { 65 | apex.Handle(h) 66 | } 67 | 68 | // Handle Dynamo events with handler. 69 | func Handle(h Handler) { 70 | HandleFunc(HandlerFunc(h.HandleDynamo)) 71 | } 72 | -------------------------------------------------------------------------------- /s3/s3.go: -------------------------------------------------------------------------------- 1 | // Package s3 provides structs for working with AWS S3 records. 2 | package s3 3 | 4 | import ( 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/apex/go-apex" 9 | ) 10 | 11 | // Event represents a S3 event with one or more records. 12 | type Event struct { 13 | Records []*Record `json:"Records"` 14 | } 15 | 16 | // Object is an S3 object. 17 | type Object struct { 18 | Key string `json:"key"` 19 | Size int `json:"size"` 20 | ETag string `json:"eTag"` 21 | VersionID string `json:"versionId"` 22 | Sequencer string `json:"sequencer"` 23 | } 24 | 25 | // Bucket is an S3 bucket. 26 | type Bucket struct { 27 | Name string `json:"name"` 28 | OwnerIdentity struct { 29 | PrincipalID string `json:"principalId"` 30 | } `json:"ownerIdentity"` 31 | ARN string `json:"arn"` 32 | } 33 | 34 | // Record represents a single S3 record. 35 | type Record struct { 36 | EventVersion string `json:"eventVersion"` 37 | EventSource string `json:"eventSource"` 38 | AWSRegion string `json:"awsRegion"` 39 | EventTime time.Time `json:"eventTime"` 40 | EventName string `json:"eventName"` 41 | UserIdentity struct { 42 | PrincipalID string `json:"principalId"` 43 | } `json:"userIdentity"` 44 | RequestParameters struct { 45 | SourceIPAddress string `json:"sourceIPAddress"` 46 | } `json:"requestParameters"` 47 | S3 struct { 48 | SchemaVersion string `json:"s3SchemaVersion"` 49 | ConfigurationID string `json:"configurationId"` 50 | Bucket *Bucket `json:"bucket"` 51 | Object *Object `json:"object"` 52 | } 53 | } 54 | 55 | // Handler handles S3 events. 56 | type Handler interface { 57 | HandleS3(*Event, *apex.Context) error 58 | } 59 | 60 | // HandlerFunc unmarshals S3 events before passing control. 61 | type HandlerFunc func(*Event, *apex.Context) error 62 | 63 | // Handle implements apex.Handler. 64 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 65 | var event Event 66 | 67 | if err := json.Unmarshal(data, &event); err != nil { 68 | return nil, err 69 | } 70 | 71 | return nil, h(&event, ctx) 72 | } 73 | 74 | // HandleFunc handles S3 events with callback function. 75 | func HandleFunc(h HandlerFunc) { 76 | apex.Handle(h) 77 | } 78 | 79 | // Handle S3 events with handler. 80 | func Handle(h Handler) { 81 | HandleFunc(HandlerFunc(h.HandleS3)) 82 | } 83 | -------------------------------------------------------------------------------- /logs/logs.go: -------------------------------------------------------------------------------- 1 | // Package logs provides structs for working with AWS CloudWatch Logs records. 2 | package logs 3 | 4 | import ( 5 | "bytes" 6 | "compress/gzip" 7 | "encoding/json" 8 | 9 | "github.com/apex/go-apex" 10 | ) 11 | 12 | // LogEvent represents a single log event. 13 | type LogEvent struct { 14 | ID string `json:"id"` 15 | Timestamp int64 `json:"timestamp"` 16 | Message string `json:"message"` 17 | } 18 | 19 | // Record represents a Cloudwatch logs event with one or more records. 20 | type Record struct { 21 | AWSLogs struct { 22 | Data []byte `json:"data"` 23 | } `json:"awslogs"` 24 | } 25 | 26 | // Event represents a single log record. 27 | type Event struct { 28 | Owner string `json:"owner"` 29 | LogGroup string `json:"logGroup"` 30 | LogStream string `json:"logStream"` 31 | SubscriptionFilters []string `json:"subscriptionFilters"` 32 | MessageType string `json:"messageType"` 33 | LogEvents []*LogEvent `json:"logEvents"` 34 | } 35 | 36 | // Handler handles Logs events. 37 | type Handler interface { 38 | HandleLogs(*Event, *apex.Context) error 39 | } 40 | 41 | // HandlerFunc unmarshals Logs events before passing control. 42 | type HandlerFunc func(*Event, *apex.Context) error 43 | 44 | // Handle implements apex.Handler. 45 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 46 | event, record := new(Event), new(Record) 47 | 48 | if err := json.Unmarshal(data, record); err != nil { 49 | return nil, err 50 | } 51 | 52 | if err := decode(record, event); err != nil { 53 | return nil, err 54 | } 55 | 56 | if err := h(event, ctx); err != nil { 57 | return nil, err 58 | } 59 | 60 | return event, nil 61 | } 62 | 63 | // HandleFunc handles Logs events with callback function. 64 | func HandleFunc(h HandlerFunc) { 65 | apex.Handle(h) 66 | } 67 | 68 | // Handle Logs events with handler. 69 | func Handle(h Handler) { 70 | HandleFunc(HandlerFunc(h.HandleLogs)) 71 | } 72 | 73 | // decode decodes the log payload which is gzipped. 74 | func decode(record *Record, event *Event) error { 75 | r, err := gzip.NewReader(bytes.NewReader(record.AWSLogs.Data)) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if err = json.NewDecoder(r).Decode(&event); err != nil { 81 | return err 82 | } 83 | 84 | if err := r.Close(); err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /proxy/decoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Alsanium, SAS. or its affiliates. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // Originally from https://github.com/eawsy/aws-lambda-go-event/blob/master/service/lambda/runtime/event/apigatewayproxyevt/decoder.go 17 | // Changes (kothar - 2017-02): 18 | // Relocated to go-apex/proxy 19 | 20 | package proxy 21 | 22 | import "encoding/json" 23 | 24 | type requestContextAlias RequestContext 25 | 26 | type authorizer map[string]string 27 | 28 | // UnmarshalJSON interprets the data as a dynamic map which may carry either a 29 | // Amazon Cognito set of claims or a custom set of attributes. It then choose 30 | // the good one at runtime and fill the authorizer with it. 31 | func (a *authorizer) UnmarshalJSON(data []byte) error { 32 | var cognito struct { 33 | Claims *map[string]string 34 | } 35 | var custom map[string]string 36 | 37 | err := json.Unmarshal(data, &cognito) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if cognito.Claims != nil { 43 | *a = authorizer(*cognito.Claims) 44 | return nil 45 | } 46 | 47 | err = json.Unmarshal(data, &custom) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | *a = authorizer(custom) 53 | return nil 54 | } 55 | 56 | type jsonRequestContext struct { 57 | *requestContextAlias 58 | Authorizer authorizer 59 | } 60 | 61 | // UnmarshalJSON interprets data as a RequestContext with a special authorizer. 62 | // It then leverages type aliasing and struct embedding to fill RequestContext 63 | // with an usual map[string]string. 64 | func (rc *RequestContext) UnmarshalJSON(data []byte) error { 65 | var jrc jsonRequestContext 66 | if err := json.Unmarshal(data, &jrc); err != nil { 67 | return err 68 | } 69 | 70 | *rc = *(*RequestContext)(jrc.requestContextAlias) 71 | rc.Authorizer = jrc.Authorizer 72 | 73 | return nil 74 | } 75 | 76 | // MarshalJSON reverts the effect of type aliasing and struct embedding used 77 | // during the marshalling step to make the pattern seamless. 78 | func (rc *RequestContext) MarshalJSON() ([]byte, error) { 79 | return json.Marshal(&jsonRequestContext{ 80 | (*requestContextAlias)(rc), 81 | rc.Authorizer, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /proxy/request.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Alsanium, SAS. or its affiliates. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // Adapted from https://github.com/eawsy/aws-lambda-go-net/blob/master/service/lambda/runtime/net/apigatewayproxy/server.go 17 | // Changes (kothar - 2017-02): 18 | // - Relocated to go-apex/proxy 19 | // - All code not related to constructing an http.Request removed 20 | // - Remaining code placed in buildRequest function 21 | // - Slight reorganisation of buildRequest to add comments 22 | 23 | package proxy 24 | 25 | import ( 26 | "encoding/base64" 27 | "encoding/json" 28 | "fmt" 29 | "net/http" 30 | "net/url" 31 | "strings" 32 | 33 | "github.com/apex/go-apex" 34 | ) 35 | 36 | // Constructs an http.Request object from a proxyEvent 37 | func buildRequest(proxyEvent *Event, ctx *apex.Context) (*http.Request, error) { 38 | // Reconstruct the request URL 39 | u, err := url.Parse(proxyEvent.Path) 40 | if err != nil { 41 | return nil, fmt.Errorf("Parse request path: %s", err) 42 | } 43 | q := u.Query() 44 | for k, v := range proxyEvent.QueryStringParameters { 45 | q.Set(k, v) 46 | } 47 | u.RawQuery = q.Encode() 48 | 49 | // Decode the request body 50 | dec := proxyEvent.Body 51 | if proxyEvent.IsBase64Encoded { 52 | data, err2 := base64.StdEncoding.DecodeString(dec) 53 | if err2 != nil { 54 | return nil, fmt.Errorf("Decode base64 request body: %s", err2) 55 | } 56 | dec = string(data) 57 | } 58 | 59 | // Create a new request object 60 | req, err := http.NewRequest(proxyEvent.HTTPMethod, u.String(), strings.NewReader(dec)) 61 | if err != nil { 62 | return nil, fmt.Errorf("Create request: %s", err) 63 | } 64 | 65 | // Copy event headers to request 66 | for k, v := range proxyEvent.Headers { 67 | req.Header.Set(k, v) 68 | } 69 | 70 | // Store the original event and context in the request headers 71 | proxyEvent.Body = "... truncated" 72 | hbody, err := json.Marshal(proxyEvent) 73 | if err != nil { 74 | return nil, fmt.Errorf("Marshal proxy event: %s", err) 75 | } 76 | req.Header.Set("X-ApiGatewayProxy-Event", string(hbody)) 77 | if ctx != nil { 78 | req.Header.Set("X-ApiGatewayProxy-Context", string(ctx.ClientContext)) 79 | } 80 | 81 | // Map additional request information 82 | req.Host = proxyEvent.Headers["Host"] 83 | 84 | return req, nil 85 | } 86 | -------------------------------------------------------------------------------- /ses/ses.go: -------------------------------------------------------------------------------- 1 | // Package ses provides structs for working with AWS SES records. 2 | package ses 3 | 4 | import ( 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/apex/go-apex" 9 | ) 10 | 11 | // Event represents a SES event with one or more records. 12 | type Event struct { 13 | Records []*Record `json:"Records"` 14 | } 15 | 16 | // Record represents a single SES record. 17 | type Record struct { 18 | EventSource string `json:"eventSource"` 19 | EventVersion string `json:"eventVersion"` 20 | SES struct { 21 | Receipt struct { 22 | Type string `json:"Type"` 23 | MessageID string `json:"MessageID"` 24 | TopicARN string `json:"TopicArn"` 25 | Subject string `json:"Subject"` 26 | Message []byte `json:"Message"` 27 | Timestamp time.Time `json:"Timestamp"` 28 | SignatureVersion string `json:"SignatureVersion"` 29 | Signature string `json:"Signature"` 30 | SignatureCertURL string `json:"SignatureCertURL"` 31 | UnsubscribeURL string `json:"UnsubscribeURL"` 32 | MessageAttributes map[string]interface{} `json:"MessageAttributes"` 33 | } `json:"receipt"` 34 | Mail struct { 35 | Type string `json:"Type"` 36 | MessageID string `json:"MessageID"` 37 | TopicARN string `json:"TopicArn"` 38 | Subject string `json:"Subject"` 39 | Message []byte `json:"Message"` 40 | Timestamp time.Time `json:"Timestamp"` 41 | SignatureVersion string `json:"SignatureVersion"` 42 | Signature string `json:"Signature"` 43 | SignatureCertURL string `json:"SignatureCertURL"` 44 | UnsubscribeURL string `json:"UnsubscribeURL"` 45 | MessageAttributes map[string]interface{} `json:"MessageAttributes"` 46 | } `json:"mail"` 47 | } `json:"ses"` 48 | } 49 | 50 | // Handler handles SES events. 51 | type Handler interface { 52 | HandleSES(*Event, *apex.Context) error 53 | } 54 | 55 | // HandlerFunc unmarshals SES events before passing control. 56 | type HandlerFunc func(*Event, *apex.Context) error 57 | 58 | // Handle implements apex.Handler. 59 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 60 | var event Event 61 | 62 | if err := json.Unmarshal(data, &event); err != nil { 63 | return nil, err 64 | } 65 | 66 | return nil, h(&event, ctx) 67 | } 68 | 69 | // HandleFunc handles SES events with callback function. 70 | func HandleFunc(h HandlerFunc) { 71 | apex.Handle(h) 72 | } 73 | 74 | // Handle SES events with handler. 75 | func Handle(h Handler) { 76 | HandleFunc(HandlerFunc(h.HandleSES)) 77 | } 78 | -------------------------------------------------------------------------------- /proxy/README.md: -------------------------------------------------------------------------------- 1 | 2 | # API Gateway Proxy request handling support 3 | 4 | This package provides an Apex-compatible handler for [proxy requests](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html). 5 | 6 | Each proxy request matches a wildcard path in AWS API Gateway, which is then converted to a standard `http.Request` before dispatching using the `http.Handler` interface. 7 | 8 | 9 | ## Usage 10 | 11 | Any router or middleware framework supporting the `http.Handler` interface should be compatible with this adapter. 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "net/http" 18 | 19 | "github.com/apex/go-apex" 20 | "github.com/apex/go-apex/proxy" 21 | ) 22 | 23 | func main() { 24 | mux := http.NewServeMux() 25 | mux.HandleFunc("/", hello) 26 | mux.HandleFunc("/foo", foo) 27 | mux.HandleFunc("/bar", bar) 28 | apex.Handle(proxy.Serve(mux)) 29 | } 30 | 31 | ... 32 | ``` 33 | 34 | 35 | ## Notes 36 | 37 | ### Stdout vs Stderr 38 | 39 | As with any Apex handler, you must make sure that your handler doesn't write anything to stdout. 40 | If your web framework logs to stdout by default, such as Martini, you need to change the logger output to use stderr. 41 | 42 | ### Content-Types passed through as plain text output: 43 | 44 | Any output with a Content-Type that doesn't match one of those listed below will be Base64 encoded in the output record. 45 | In order for this to be returned from API Gateway correctly, you will need to enable binary support and 46 | map the content types containing binary data. 47 | 48 | In practice you can usually map all types as binary using the `*/*` pattern for binary support if you aren't using other 49 | API Gateway resources which conflict with this. 50 | 51 | The text-mode Content-Type regular expressions used by default are: 52 | 53 | * text/.* 54 | * application/json 55 | * application/.*\+json 56 | * application/xml 57 | * application/.*\+xml 58 | 59 | You can override this by calling `proxy.SetTextContentTypes` with a list of regular expressions matching the types that should 60 | not be Base64 encoded. 61 | 62 | ### Output encoding 63 | API gateway will automatically gzip-encode the output of your API, so it's not necessary to gzip the output of your webapp. 64 | 65 | If you use your own gzip encoding, it's likely to interfere with the Base64 output for text content types - this hasn't been tested. 66 | 67 | ## Differences from eawsy 68 | 69 | This implementation reuses a large portion of the event definitions from the eawsy AWS Lambda projects: 70 | 71 | * https://github.com/eawsy/aws-lambda-go-event 72 | * https://github.com/eawsy/aws-lambda-go-net 73 | 74 | However, it wraps a web application in an apex-compatible adapter which makes direct calls to an http.Handler instance 75 | rather than creating a fake `net.Conn` and marshalling/unmarshalling the request data. 76 | 77 | A ResponseWriter implementation captures the response of the handler and constructs an API Gateway Proxy response. 78 | -------------------------------------------------------------------------------- /cloudwatch/cloudwatch.go: -------------------------------------------------------------------------------- 1 | package cloudwatch 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/apex/go-apex" 8 | ) 9 | 10 | // Event represents a CloudWatch Event 11 | type Event struct { 12 | ID string `json:"id"` 13 | DetailType string `json:"detail-type"` 14 | Source string `json:"source"` 15 | Account string `json:"account"` 16 | Time time.Time `json:"time"` 17 | Region string `json:"region"` 18 | Resources []string `json:"resources"` 19 | Detail json.RawMessage `json:"detail"` 20 | } 21 | 22 | // AutoScalingGroupDetail of the triggered event 23 | type AutoScalingGroupDetail struct { 24 | ActivityID string `json:"ActivityId"` 25 | AutoScalingGroupName string `json:"AutoScalingGroupName"` 26 | Cause string `json:"Cause"` 27 | Details map[string]string `json:"Details"` 28 | EC2InstanceID string `json:"EC2InstanceId"` 29 | RequestID string `json:"RequestId"` 30 | StatusCode string `json:"StatusCode"` 31 | 32 | StartTime time.Time `json:"StartTime"` 33 | EndTime time.Time `json:"EndTime"` 34 | } 35 | 36 | // EC2Detail of the triggered event 37 | type EC2Detail struct { 38 | InstanceID string `json:"instance-id"` 39 | State string `json:"state"` 40 | } 41 | 42 | // APIDetail of the triggered event 43 | // This is useful for API or Console events 44 | type APIDetail struct { 45 | EventID string `json:"eventID"` 46 | EventName string `json:"eventName"` 47 | EventSource string `json:"eventSource"` 48 | EventTime time.Time `json:"eventTime"` 49 | EventType string `json:"eventType"` 50 | EventVersion string `json:"eventVersion"` 51 | 52 | AWSRegion string `json:"awsRegion"` 53 | AdditionalEventData map[string]string `json:"additionalEventData,omitempty"` 54 | RequestParams interface{} `json:"requestParameters"` 55 | ResponseElements map[string]string `json:"responseElements,omitempty"` 56 | SourceIPAddress string `json:"sourceIPAddress"` 57 | UserAgent string `json:"userAgent"` 58 | UserIdentity UserIdentity `json:"userIdentity,omitempty"` 59 | } 60 | 61 | type UserIdentity struct { 62 | Type string `json:"type,omitempty"` 63 | PrincipleID string `json:"principalId,omitempty"` 64 | ARN string `json:"arn,omitempty"` 65 | AccountID string `json:"accountId,omitempty"` 66 | SessionContext map[string]string `json:"sessionContext,omitempty"` 67 | } 68 | 69 | // Handler handles CloudWatch Events 70 | type Handler interface { 71 | HandleCloudWatchEvent(*Event, *apex.Context) error 72 | } 73 | 74 | // HandlerFunc unmarshals CloudWatch Events before passing control. 75 | type HandlerFunc func(*Event, *apex.Context) error 76 | 77 | // Handle implements apex.Handler. 78 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 79 | var event Event 80 | 81 | if err := json.Unmarshal(data, &event); err != nil { 82 | return nil, err 83 | } 84 | 85 | return nil, h(&event, ctx) 86 | } 87 | 88 | // HandleFunc handles CloudWatch Events with callback function. 89 | func HandleFunc(h HandlerFunc) { 90 | apex.Handle(h) 91 | } 92 | 93 | // Handle CloudWatch Events with handler. 94 | func Handle(h Handler) { 95 | HandleFunc(HandlerFunc(h.HandleCloudWatchEvent)) 96 | } 97 | -------------------------------------------------------------------------------- /apex.go: -------------------------------------------------------------------------------- 1 | // Package apex provides Lambda support for Go via a 2 | // Node.js shim and this package for operating over 3 | // stdio. 4 | package apex 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | "log" 10 | "os" 11 | ) 12 | 13 | // Handler handles Lambda events. 14 | type Handler interface { 15 | Handle(json.RawMessage, *Context) (interface{}, error) 16 | } 17 | 18 | // HandlerFunc implements Handler. 19 | type HandlerFunc func(json.RawMessage, *Context) (interface{}, error) 20 | 21 | // Handle Lambda event. 22 | func (h HandlerFunc) Handle(event json.RawMessage, ctx *Context) (interface{}, error) { 23 | return h(event, ctx) 24 | } 25 | 26 | // Context represents the context data provided by a Lambda invocation. 27 | type Context struct { 28 | InvokeID string `json:"invokeid"` 29 | RequestID string `json:"awsRequestId"` 30 | FunctionName string `json:"functionName"` 31 | FunctionVersion string `json:"functionVersion"` 32 | LogGroupName string `json:"logGroupName"` 33 | LogStreamName string `json:"logStreamName"` 34 | MemoryLimitInMB string `json:"memoryLimitInMB"` 35 | IsDefaultFunctionVersion bool `json:"isDefaultFunctionVersion"` 36 | ClientContext json.RawMessage `json:"clientContext"` 37 | Identity Identity `json:"identity,omitempty"` 38 | InvokedFunctionARN string `json:"invokedFunctionArn"` 39 | } 40 | 41 | // Identity as defined in: http://docs.aws.amazon.com/mobile/sdkforandroid/developerguide/lambda.html#identity-context 42 | type Identity struct { 43 | CognitoIdentityID string `json:"cognitoIdentityId"` 44 | CognitoIdentityIDPoolID string `json:"cognitoIdentityPoolId"` 45 | } 46 | 47 | // Handle Lambda events with the given handler. 48 | func Handle(h Handler) { 49 | m := &manager{ 50 | Reader: os.Stdin, 51 | Writer: os.Stdout, 52 | Handler: h, 53 | } 54 | 55 | m.Start() 56 | } 57 | 58 | // HandleFunc handles Lambda events with the given handler function. 59 | func HandleFunc(h HandlerFunc) { 60 | Handle(h) 61 | } 62 | 63 | // input from the node shim. 64 | type input struct { 65 | // ID is an identifier that is boomeranged back to the called, 66 | // to allow for concurrent commands 67 | ID string `json:"id,omitempty"` 68 | Event json.RawMessage `json:"event"` 69 | Context *Context `json:"context"` 70 | } 71 | 72 | // output for the node shim. 73 | type output struct { 74 | // The boomeranged ID from the caller 75 | ID string `json:"id,omitempty"` 76 | Error string `json:"error,omitempty"` 77 | Value interface{} `json:"value,omitempty"` 78 | } 79 | 80 | // manager for operating over stdio. 81 | type manager struct { 82 | Reader io.Reader 83 | Writer io.Writer 84 | Handler Handler 85 | } 86 | 87 | // Start the manager. 88 | func (m *manager) Start() { 89 | dec := json.NewDecoder(m.Reader) 90 | enc := json.NewEncoder(m.Writer) 91 | 92 | for { 93 | var msg input 94 | err := dec.Decode(&msg) 95 | 96 | if err == io.EOF { 97 | break 98 | } 99 | 100 | if err != nil { 101 | log.Printf("error decoding input: %s", err) 102 | break 103 | } 104 | 105 | v, err := m.Handler.Handle(msg.Event, msg.Context) 106 | out := output{ID: msg.ID, Value: v} 107 | 108 | if err != nil { 109 | out.Error = err.Error() 110 | } 111 | 112 | if err := enc.Encode(out); err != nil { 113 | log.Printf("error encoding output: %s", err) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /cloudformation/handler.go: -------------------------------------------------------------------------------- 1 | // Package cloudformation provides structs for working with AWS CloudFormation custom resources. 2 | package cloudformation 3 | 4 | // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "log" 10 | "net/http" 11 | 12 | "github.com/apex/go-apex" 13 | ) 14 | 15 | // Request is a request to a CloudFormation Custom Resource 16 | type Request struct { 17 | RequestType string `json:"RequestType"` 18 | ResponseURL string `json:"ResponseURL"` 19 | StackID string `json:"StackId"` 20 | RequestID string `json:"RequestId"` 21 | ResourceType string `json:"ResourceType"` 22 | LogicalResourceID string `json:"LogicalResourceId"` 23 | ResourceProperties map[string]interface{} `json:"ResourceProperties"` 24 | } 25 | 26 | // Response is the response sent to the ResponseURL of the CloudFormation service 27 | type Response struct { 28 | Status string `json:"Status"` 29 | Reason string `json:"Reason"` 30 | PhysicalResourceID string `json:"PhysicalResourceId"` 31 | StackID string `json:"StackId"` 32 | RequestID string `json:"RequestId"` 33 | LogicalResourceID string `json:"LogicalResourceId"` 34 | Data interface{} 35 | } 36 | 37 | func buildResponse(req Request, ctx *apex.Context) Response { 38 | return Response{ 39 | RequestID: req.RequestID, 40 | Status: "SUCCESS", 41 | Reason: "See the details in CloudWatch Log Stream: " + ctx.LogStreamName, 42 | PhysicalResourceID: ctx.LogStreamName, 43 | StackID: req.StackID, 44 | LogicalResourceID: req.LogicalResourceID, 45 | } 46 | } 47 | 48 | func sendResponse(resp Response, url string) error { 49 | b, err := json.Marshal(resp) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer(b)) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | cfnResp, err := http.DefaultClient.Do(req) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // TODO: Check status of return 65 | defer cfnResp.Body.Close() 66 | return nil 67 | } 68 | 69 | // Handler handles CloudFormation Custom Resource events. 70 | type Handler interface { 71 | HandleCloudFormation(*Request, *apex.Context) (interface{}, error) 72 | } 73 | 74 | // HandlerFunc unmarshals CloudFormation Requests before passing control. 75 | type HandlerFunc func(*Request, *apex.Context) (interface{}, error) 76 | 77 | // Handle implements apex.Handler. 78 | func (h HandlerFunc) Handle(rawReq json.RawMessage, ctx *apex.Context) (interface{}, error) { 79 | var req Request 80 | 81 | if err := json.Unmarshal(rawReq, &req); err != nil { 82 | return nil, err 83 | } 84 | 85 | log.Printf("Request %#v", req) 86 | 87 | data, err := h(&req, ctx) 88 | resp := buildResponse(req, ctx) 89 | resp.Data = data 90 | 91 | if err != nil { 92 | resp.Status = "FAILURE" 93 | resp.Reason = err.Error() 94 | } 95 | 96 | log.Printf("Response %#v", resp) 97 | log.Printf("Data %#v", data) 98 | 99 | return data, sendResponse(resp, req.ResponseURL) 100 | } 101 | 102 | // HandleFunc handles CloudFormation Custom Resource events with a callback function. 103 | func HandleFunc(h HandlerFunc) { 104 | apex.Handle(h) 105 | } 106 | 107 | // Handle CloudFormation Custom Resource events with a handler. 108 | func Handle(h Handler) { 109 | HandleFunc(HandlerFunc(h.HandleCloudFormation)) 110 | } 111 | -------------------------------------------------------------------------------- /apiai/apiai_test.go: -------------------------------------------------------------------------------- 1 | package apiai_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/apex/go-apex" 8 | "github.com/apex/go-apex/apiai" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // HandlerFunc apex.Handler assertion. 13 | var _ apex.Handler = apiai.HandlerFunc(func(event *apiai.Event, ctx *apex.Context) (interface{}, error) { 14 | return nil, nil 15 | }) 16 | 17 | func fixture(path string) []byte { 18 | b, err := ioutil.ReadFile(path) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | return b 24 | } 25 | 26 | var event = []byte(` 27 | { 28 | "id": "9b49f2fb-fdd4-46f1-aa0d-7c4ed2caccdc", 29 | "timestamp": "2016-09-08T05:34:23.167Z", 30 | "result": { 31 | "source": "agent", 32 | "resolvedQuery": "my name is Sam and I live in Paris", 33 | "action": "", 34 | "actionIncomplete": false, 35 | "parameters": { 36 | "city": "Paris", 37 | "user_name": "Sam" 38 | }, 39 | "contexts": [ 40 | { 41 | "name": "greetings", 42 | "parameters": { 43 | "city": "Paris", 44 | "user_name": "Sam", 45 | "city.original": "Paris", 46 | "user_name.original": "Sam" 47 | }, 48 | "lifespan": 5 49 | } 50 | ], 51 | "metadata": { 52 | "intentId": "373a354b-c15a-4a60-ac9d-a9f2aee76cb4", 53 | "webhookUsed": "false", 54 | "intentName": "greetings" 55 | }, 56 | "fulfillment": { 57 | "speech": "Nice to meet you, Sam!" 58 | }, 59 | "score": 1 60 | }, 61 | "status": { 62 | "code": 200, 63 | "errorType": "success" 64 | }, 65 | "sessionId": "7501656c-b86e-496f-ae03-c2c800b851ff" 66 | } 67 | `) 68 | 69 | func TestHandlerFunc_Handle(t *testing.T) { 70 | called := false 71 | 72 | fn := func(e *apiai.Event, c *apex.Context) (interface{}, error) { 73 | called = true 74 | 75 | assert.Equal(t, "9b49f2fb-fdd4-46f1-aa0d-7c4ed2caccdc", e.ID) 76 | assert.Equal(t, "7501656c-b86e-496f-ae03-c2c800b851ff", e.SessionID) 77 | assert.Equal(t, "2016-09-08T05:34:23.167Z", e.Timestamp) 78 | 79 | r := e.Result 80 | assert.Equal(t, "agent", r.Source) 81 | assert.Equal(t, "my name is Sam and I live in Paris", r.ResolvedQuery) 82 | assert.Equal(t, "", r.Action) 83 | assert.IsType(t, true, r.ActionIncomplete) 84 | assert.Equal(t, false, r.ActionIncomplete) 85 | 86 | assert.Equal(t, "Paris", r.Parameters["city"]) 87 | assert.Equal(t, "Sam", r.Parameters["user_name"]) 88 | 89 | ctx := r.Contexts 90 | assert.NotEmpty(t, ctx) 91 | 92 | grCtx := ctx[0] 93 | assert.NotEmpty(t, ctx[0]) 94 | assert.Equal(t, "greetings", grCtx.Name) 95 | assert.IsType(t, 1, grCtx.Lifespan) 96 | assert.Equal(t, 5, grCtx.Lifespan) 97 | 98 | assert.Equal(t, "Paris", grCtx.Parameters["city"]) 99 | assert.Equal(t, "Sam", grCtx.Parameters["user_name"]) 100 | assert.Equal(t, "Paris", grCtx.Parameters["city.original"]) 101 | assert.Equal(t, "Sam", grCtx.Parameters["user_name.original"]) 102 | 103 | m := r.Metadata 104 | assert.Equal(t, "373a354b-c15a-4a60-ac9d-a9f2aee76cb4", m.IntentID) 105 | assert.Equal(t, "false", m.WebHookUsed) 106 | assert.Equal(t, "greetings", m.IntentName) 107 | 108 | f := r.Fulfillment 109 | assert.Equal(t, "Nice to meet you, Sam!", f.Speech) 110 | 111 | s := e.Status 112 | assert.IsType(t, 1, s.Code) 113 | assert.Equal(t, 200, s.Code) 114 | assert.Equal(t, "success", s.ErrorType) 115 | 116 | return nil, nil 117 | } 118 | 119 | _, err := apiai.HandlerFunc(fn).Handle(event, nil) 120 | assert.NoError(t, err) 121 | 122 | assert.True(t, called, "function never called") 123 | } 124 | -------------------------------------------------------------------------------- /apiai/apiai.go: -------------------------------------------------------------------------------- 1 | package apiai 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/apex/go-apex" 7 | ) 8 | 9 | // Context is... 10 | type Context struct { 11 | // Context name 12 | Name string `json:"name"` 13 | // Object consisting of "parameter_name":"parameter_value" and "parameter_name.original":"original_parameter_value" pairs. 14 | Parameters map[string]string `json:"parameters"` 15 | // Number of requests after which the context will expire. 16 | Lifespan int `json:"lifespan"` 17 | } 18 | 19 | // Fullfillment is 20 | type Fullfillment struct { 21 | // Text to be pronounced to the user / shown on the screen 22 | Speech string `json:"speech"` 23 | // Matching score for the intent. 24 | Score float64 `json:"score,omitempty"` 25 | } 26 | 27 | // Event data JSON 28 | // https://docs.api.ai/docs/webhook 29 | // https://docs.api.ai/docs/query#response 30 | type Event struct { 31 | 32 | // Unique identifier of the result. 33 | ID string `json:"id"` 34 | 35 | // Date and time of the request in UTC timezone using ISO-8601 format. 36 | Timestamp string `json:"timestamp"` 37 | 38 | // Contains the results of the natual language processing. 39 | Result struct { 40 | // Source of the answer. Could be "agent" if the response was retrieved from the agent. Or "domains", if Domains functionality is enabled and the source is one of the domains 41 | Source string `json:"source"` 42 | 43 | // The query that was used to produce this result. 44 | ResolvedQuery string `json:"resolvedQuery"` 45 | 46 | // An action to take. 47 | Action string `json:"action"` 48 | 49 | // true if the triggered intent has required parameters and not all the required parameter values have been collected 50 | // false if all required parameter values have been collected or if the triggered intent doesn't containt any required parameters 51 | ActionIncomplete bool `json:"actionIncomplete"` 52 | 53 | // Object consisting of "parameter_name":"parameter_value" pairs. 54 | Parameters map[string]string `json:"parameters"` 55 | 56 | Contexts []*Context `json:"contexts"` 57 | 58 | Metadata struct { 59 | // ID of the intent that produced this result. 60 | IntentID string `json:"intentId"` 61 | // Name of the intent that produced this result. 62 | IntentName string `json:"intentName"` 63 | // Indicates wheather webhook functionaly is enabled in the triggered intent. 64 | WebHookUsed string `json:"webhookUsed"` 65 | } `json:"metadata,omitempty"` 66 | Fulfillment *Fullfillment `json:"fulfillment"` 67 | } `json:"result"` 68 | 69 | // Contains data on how the request succeeded or failed. 70 | // https://docs.api.ai/docs/status-object 71 | Status struct { 72 | // HTTP status code 73 | Code int `json:"code"` 74 | 75 | // Text description of error, or "success" if no error. 76 | ErrorType string `json:"errorType"` 77 | 78 | // ID of the error. Optionally returned if the request failed. 79 | ErrorID string `json:"errorId"` 80 | 81 | // Text details of the error. Only returned if the request failed. 82 | ErrorDetails string `json:"errorDetails"` 83 | } `json:"status,omitempty"` 84 | 85 | // Session ID. 86 | SessionID string `json:"sessionId,omitempty"` 87 | } 88 | 89 | // ResponseMessage is JSON. 90 | // @url https://docs.api.ai/docs/webhook 91 | type ResponseMessage struct { 92 | 93 | // Voice response to the request. 94 | Speech string `json:"speech"` 95 | 96 | // Text displayed on the user device screen. 97 | DisplayText string `json:"displayText"` 98 | 99 | // Additional data required for performing the action on the client side. 100 | // The data is sent to the client in the original form and is not processed by Api.ai. 101 | Data map[string]string `json:"data"` 102 | 103 | // Data source 104 | Source string `json:"source"` 105 | 106 | ContextOut []*Context `json:"contextOut"` 107 | } 108 | 109 | // Handler handles api.ai Events 110 | type Handler interface { 111 | HandleApiaiEvent(*Event, *apex.Context) (interface{}, error) 112 | } 113 | 114 | // HandlerFunc unmarshals Slack Events before passing control. 115 | type HandlerFunc func(*Event, *apex.Context) (interface{}, error) 116 | 117 | // Handle implements apex.Handler. 118 | func (h HandlerFunc) Handle(data json.RawMessage, ctx *apex.Context) (interface{}, error) { 119 | var event Event 120 | 121 | if err := json.Unmarshal(data, &event); err != nil { 122 | return nil, err 123 | } 124 | 125 | return h(&event, ctx) 126 | } 127 | 128 | // HandleFunc handles Api.ai Events with callback function. 129 | func HandleFunc(h HandlerFunc) { 130 | apex.Handle(h) 131 | } 132 | 133 | // Handle Api.ai Events with handler. 134 | func Handle(h Handler) { 135 | HandleFunc(HandlerFunc(h.HandleApiaiEvent)) 136 | } 137 | -------------------------------------------------------------------------------- /proxy/responsewriter.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "log" 7 | "net/http" 8 | "regexp" 9 | ) 10 | 11 | // DefaultTextContentTypes specifies the content types that will not be Base64 encoded 12 | // by default. See SetTextContentTypes 13 | var DefaultTextContentTypes = []string{ 14 | `text/.*`, 15 | `application/json`, 16 | `application/.*\+json`, 17 | `application/xml`, 18 | `application/.*\+xml`, 19 | } 20 | 21 | var textContentTypes []string 22 | var textContentTypesRegexp *regexp.Regexp 23 | 24 | func init() { 25 | err := SetTextContentTypes(DefaultTextContentTypes) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | } 30 | 31 | // SetTextContentTypes configures the proxy package to skip Base64 encoding of the response 32 | // body for responses with a Content-Type header matching one of the provided types. 33 | // Each type provided is a regular expression pattern. 34 | func SetTextContentTypes(types []string) error { 35 | pattern := "(" + types[0] 36 | for _, t := range types { 37 | pattern += "|" + t 38 | } 39 | pattern += `)\b.*` 40 | 41 | r, err := regexp.Compile(pattern) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | textContentTypesRegexp = r 47 | return nil 48 | } 49 | 50 | // Response defines parameters for a well formed response AWS Lambda should 51 | // return to Amazon API Gateway. 52 | // Originally from https://github.com/eawsy/aws-lambda-go-net/blob/master/service/lambda/runtime/net/apigatewayproxy/server.go 53 | type Response struct { 54 | StatusCode int `json:"statusCode"` 55 | Headers map[string]string `json:"headers,omitempty"` 56 | Body string `json:"body,omitempty"` 57 | IsBase64Encoded bool `json:"isBase64Encoded"` 58 | } 59 | 60 | // ResponseWriter implements the http.ResponseWriter interface and 61 | // collects the results of an HTTP request in an API Gateway proxy 62 | // response object. 63 | type ResponseWriter struct { 64 | response Response 65 | output bytes.Buffer 66 | headers http.Header 67 | headersWritten bool 68 | } 69 | 70 | // Header returns the header map that will be sent by 71 | // WriteHeader. Changing the header after a call to 72 | // WriteHeader (or Write) has no effect unless the modified 73 | // headers were declared as trailers by setting the 74 | // "Trailer" header before the call to WriteHeader (see example). 75 | // To suppress implicit response headers, set their value to nil. 76 | func (w *ResponseWriter) Header() http.Header { 77 | if w.headers == nil { 78 | w.headers = make(http.Header) 79 | } 80 | return w.headers 81 | } 82 | 83 | // Write writes the data to the connection as part of an HTTP reply. 84 | // 85 | // If WriteHeader has not yet been called, Write calls 86 | // WriteHeader(http.StatusOK) before writing the data. If the Header 87 | // does not contain a Content-Type line, Write adds a Content-Type set 88 | // to the result of passing the initial 512 bytes of written data to 89 | // DetectContentType. 90 | // 91 | // Depending on the HTTP protocol version and the client, calling 92 | // Write or WriteHeader may prevent future reads on the 93 | // Request.Body. For HTTP/1.x requests, handlers should read any 94 | // needed request body data before writing the response. Once the 95 | // headers have been flushed (due to either an explicit Flusher.Flush 96 | // call or writing enough data to trigger a flush), the request body 97 | // may be unavailable. For HTTP/2 requests, the Go HTTP server permits 98 | // handlers to continue to read the request body while concurrently 99 | // writing the response. However, such behavior may not be supported 100 | // by all HTTP/2 clients. Handlers should read before writing if 101 | // possible to maximize compatibility. 102 | func (w *ResponseWriter) Write(bs []byte) (int, error) { 103 | if !w.headersWritten { 104 | w.WriteHeader(http.StatusOK) 105 | } 106 | return w.output.Write(bs) 107 | } 108 | 109 | // WriteHeader sends an HTTP response header with status code. 110 | // If WriteHeader is not called explicitly, the first call to Write 111 | // will trigger an implicit WriteHeader(http.StatusOK). 112 | // Thus explicit calls to WriteHeader are mainly used to 113 | // send error codes. 114 | func (w *ResponseWriter) WriteHeader(status int) { 115 | if w.headersWritten { 116 | return 117 | } 118 | 119 | w.response.StatusCode = status 120 | 121 | finalHeaders := make(map[string]string) 122 | for k, v := range w.headers { 123 | if len(v) > 0 { 124 | finalHeaders[k] = v[len(v)-1] 125 | } 126 | } 127 | 128 | if value, ok := finalHeaders["Content-Type"]; !ok || value == "" { 129 | finalHeaders["Content-Type"] = "text/plain; charset=utf-8" 130 | } 131 | 132 | w.response.Headers = finalHeaders 133 | 134 | w.headersWritten = true 135 | } 136 | 137 | // finish writes the accumulated output to the response.Body 138 | func (w *ResponseWriter) finish() { 139 | 140 | // Determine if we should Base64 encode the output 141 | contentType := w.response.Headers["Content-Type"] 142 | 143 | // Only encode text content types without base64 encoding 144 | w.response.IsBase64Encoded = !textContentTypesRegexp.MatchString(contentType) 145 | 146 | if w.response.IsBase64Encoded { 147 | w.response.Body = base64.StdEncoding.EncodeToString(w.output.Bytes()) 148 | } else { 149 | w.response.Body = w.output.String() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /proxy/event.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Alsanium, SAS. or its affiliates. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // Originally from https://github.com/eawsy/aws-lambda-go-event/blob/master/service/lambda/runtime/event/apigatewayproxyevt/definition.go 17 | // Changes (kothar - 2017-02): 18 | // Relocated to go-apex/proxy 19 | 20 | package proxy 21 | 22 | import "encoding/json" 23 | 24 | // Identity provides identity information about the API caller. 25 | type Identity struct { 26 | // The API owner key associated with the API. 27 | APIKey string 28 | 29 | // The AWS account ID associated with the request. 30 | AccountID string 31 | 32 | // The User Agent of the API caller. 33 | UserAgent string 34 | 35 | // The source IP address of the TCP connection making the request to 36 | // Amazon API Gateway. 37 | SourceIP string 38 | 39 | // The Amazon Access Key associated with the request. 40 | AccessKey string 41 | 42 | // The principal identifier of the caller making the request. 43 | // It is same as the User and interchangeable. 44 | Caller string 45 | 46 | // The principal identifier of the user making the request. 47 | // It is same as the Caller and interchangeable. 48 | User string 49 | 50 | // The Amazon Resource Name (ARN) of the effective user identified after 51 | // authentication. 52 | UserARN string 53 | 54 | // The Amazon Cognito identity ID of the caller making the request. 55 | // Available only if the request was signed with Amazon Cognito credentials. 56 | CognitoIdentityID string 57 | 58 | // The Amazon Cognito identity pool ID of the caller making the request. 59 | // Available only if the request was signed with Amazon Cognito credentials. 60 | CognitoIdentityPoolID string 61 | 62 | // The Amazon Cognito authentication type of the caller making the request. 63 | // Available only if the request was signed with Amazon Cognito credentials. 64 | CognitoAuthenticationType string 65 | 66 | // The Amazon Cognito authentication provider used by the caller making the 67 | // request. 68 | // Available only if the request was signed with Amazon Cognito credentials. 69 | CognitoAuthenticationProvider string 70 | } 71 | 72 | // RequestContext provides contextual information about an Amazon API Gateway 73 | // Proxy event. 74 | type RequestContext struct { 75 | // The identifier Amazon API Gateway assigns to the API. 76 | APIID string 77 | 78 | // The identifier Amazon API Gateway assigns to the resource. 79 | ResourceID string 80 | 81 | // An automatically generated ID for the API call. 82 | RequestID string 83 | 84 | // The incoming request HTTP method name. 85 | // Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. 86 | HTTPMethod string 87 | 88 | // The resource path as defined in Amazon API Gateway. 89 | ResourcePath string 90 | 91 | // The AWS account ID associated with the API. 92 | AccountID string 93 | 94 | // The deployment stage of the API call (for example, Beta or Prod). 95 | Stage string 96 | 97 | // The API caller identification information. 98 | Identity *Identity 99 | 100 | // If used with Amazon Cognito, it represents the claims returned from the 101 | // Amazon Cognito user pool after the method caller is successfully 102 | // authenticated. 103 | // If used with Amazon API Gateway custom authorizer, it represents the 104 | // specified key-value pair of the context map returned from the custom 105 | // authorizer AWS Lambda function. 106 | Authorizer map[string]string `json:"-"` 107 | } 108 | 109 | // Event represents an Amazon API Gateway Proxy Event. 110 | type Event struct { 111 | // The incoming request HTTP method name. 112 | // Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. 113 | HTTPMethod string 114 | 115 | // The incoming reauest HTTP headers. 116 | // Duplicate entries are not supported. 117 | Headers map[string]string 118 | 119 | // The resource path with raw placeholders as defined in Amazon API Gateway. 120 | Resource string 121 | 122 | // The incoming request path parameters corresponding to the resource path 123 | // placeholders values as defined in Resource. 124 | PathParameters map[string]string 125 | 126 | // The real path corresponding to the path parameters injected into the 127 | // Resource placeholders. 128 | Path string 129 | 130 | // The incoming request query string parameters. 131 | // Duplicate entries are not supported. 132 | QueryStringParameters map[string]string 133 | 134 | // If used with Amazon API Gateway binary support, it represents the Base64 135 | // encoded binary data from the client. 136 | // Otherwise it represents the raw data from the client. 137 | Body string 138 | 139 | // A flag to indicate if the applicable request payload is Base64 encoded. 140 | IsBase64Encoded bool 141 | 142 | // The name-value pairs defined as configuration attributes associated with 143 | // the deployment stage of the API. 144 | StageVariables map[string]string 145 | 146 | // The contextual information associated with the API call. 147 | RequestContext *RequestContext 148 | } 149 | 150 | // String returns the string representation. 151 | func (e *Event) String() string { 152 | s, _ := json.MarshalIndent(e, "", " ") 153 | return string(s) 154 | } 155 | 156 | // GoString returns the string representation. 157 | func (e *Event) GoString() string { 158 | return e.String() 159 | } 160 | --------------------------------------------------------------------------------