Skip to content

Added AWSLambdaEvents library #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

Conversation

fabianfett
Copy link
Member

Motivation

Lambda has a number of triggers within the AWS ecosystem, these triggers create events that are then handed to Lambda. To allow faster development with Swift and Lambda we should include as many events as possible with our Lambda.

In Go the batteries are included as well: https://github.com/aws/aws-lambda-go/tree/master/events

Changes

  • Added a library AWSLambaEvents
  • Added test target AWSLambaEventsTests
  • The AWSLambaEvents vends the fabianfett/swift-base64-kit Base64 implementation that is also vended in SwiftNIOWebsocket
  • AWSLambaEvents links Foundation (primarily for Date and DateFormatter)

Open ends

  • Currently Foundation JSONEncoder and JSONDecoder is backed into the code, I would like to change this before merge

@fabianfett fabianfett requested a review from tomerd March 12, 2020 16:22
@fabianfett fabianfett force-pushed the add-lambda-events branch 3 times, most recently from 7864e00 to 1f26995 Compare March 12, 2020 23:16
@fabianfett
Copy link
Member Author

Local sanity is happy:

$> ./scripts/sanity.sh                                                  
=> Checking linux tests... okay.
=> Checking format... okay.
=> Checking license headers
   * swift-or-c... okay.
   * bash... okay.
   * dtrace... okay.

import Foundation
import NIO

// https://github.com/aws/aws-lambda-go/blob/master/events/s3.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no better/official doc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case .binary(let byteBuffer):
let base64 = byteBuffer.withUnsafeReadableBytes { (pointer) -> String in
String(base64Encoding: pointer)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weissi not gonna like this :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is a pattern we need for base64 maybe we add a ByteBuffer extension in that area of the code to keep it DRY and minimize chances of getting unsafe access wrong


//===----------------------------------------------------------------------===//
// This is a vendored version from:
// https://github.com/fabianfett/swift-base64-kit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

faster than foundation's?

Copy link
Member Author

@fabianfett fabianfett Mar 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay. was this reviewed by the nio team already, or should I review?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the encoding was reviewed afaik, the decoding wasn't.

)

do {
let data = try JSONEncoder().encodeAsByteBuffer(resp, allocator: ByteBufferAllocator())
Copy link
Contributor

@tomerd tomerd Mar 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XCTAssertNoThrows, but then reading the value becomes a bit odd ... XCTAssertNoThrows should really be returning T

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's why most of my tests looks like this. one can of course do

func testBla() throws {
  let a = try blub()
}

But if an error is thrown the error isn't printed anywhere in a simple and easy to read way. That's why I decided to go the expressive/repetitive way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always write these tests like that now:

func testFoo() /* no throws! */ {
    var maybeThing: Thing?
    XCTAssertNoThrow(maybeThing = try makeThing())
    guard let thing = maybeThing else {
        XCTFail("couldn't make thing")
        return
   }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@tomerd tomerd Mar 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do almost the same as @weissi

var thing: Thing!
XCTAssertNoThrow(thing = try makeThing())
...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomerd do you want me to change the tests to your format?

Copy link
Contributor

@tomerd tomerd Mar 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low priority, but would be nicer imo. throwing tests are a bit of a smell

@fabianfett fabianfett force-pushed the add-lambda-events branch 5 times, most recently from 3ec24d8 to 5020b90 Compare March 16, 2020 10:58
let data = SQSTests.testPayload.data(using: .utf8)!
do {
let decoder = JSONDecoder()
let event = try decoder.decode(SQS.Event.self, from: data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case you could just write

var event: Event?
XCTAssertNoThrow(event = try decoder.decode(...)

XCTAssertEqual(1, event?.records.first)

guard let message = event?.records.first else {
    XCTFail(...)
    return
}


// https://github.com/aws/aws-lambda-go/blob/master/events/apigw.go

public enum APIGateway {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we namespace these with AWS to make sure generic terms like APIGateway that may refer to user's own code or other SDK that may be integrated into this will have enough "space"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do already have a namespace (the package-name), don't we?

public let elb: ELBContext
}

public let httpMethod: HTTPMethod
Copy link
Contributor

@tomerd tomerd Mar 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we explore NIO types like this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question. As you might have noticed that this is code copied over from my lambda runtime. The reason I choose to expose those NIO types is. I wanted to build something barebone, allowing as many use-cases as possible in an environment that people might be aware of while using only needed Foundation types.

I don't know what API design goal we have in mind for this library? AsyncHTTPClient uses the same nio types btw.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that AsyncHTTPClient and this library are at a different level of abstraction and target different audiences. I dont see a strong reason for the target audience of Lambda to need and learn about NIO in general or ByteBuffer specifically, while Data (or [Int8] if we want to avoid Foundation) is a much more natural type for them. when it comes to HTTPMethod and HTTPHeaders, I would argue that we should not leak NIO types as this is an implementation detail leaking to user's APIs, and we can easily wrap them with our own.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it makes so much sense to reimplement: HTTPMethod, HTTPStatusCode and HTTPHeaders. What do you think exporting those with:

@_exported import struct NIOHTTP1.HTTPMethod

Otherwise for some code we would just duplicate it I guess.

import struct Foundation.Data
import class Foundation.JSONDecoder

public protocol DecodableBody {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be public?

}

public struct TargetGroupResponse {
public let statusCode: HTTPResponseStatus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question on exposing NIO types

public struct TargetGroupResponse {
public let statusCode: HTTPResponseStatus
public let statusDescription: String?
public let headers: HTTPHeaders?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

statusCode: HTTPResponseStatus,
statusDescription: String? = nil,
headers: HTTPHeaders? = nil,
body: String = "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it optional?

self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)

let body = try container.decode(String.self, forKey: .body)
self.body = body != "" ? body : nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why empty string comparison?

// MARK: - Response -

extension ALB.TargetGroupResponse: Encodable {
public static let MultiValueHeadersEnabledKey =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be public?

singleValueHeaders[name] = value
}
try container.encode(singleValueHeaders, forKey: .headers)
}
Copy link
Contributor

@tomerd tomerd Mar 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you can sue reduce in some of these headers transformations

encoder: JSONEncoder = JSONEncoder()
) throws {
var headers = headers ?? HTTPHeaders()
headers.add(name: "Content-Type", value: "application/json")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you shed some light on why this is needed

Copy link
Member Author

@fabianfett fabianfett Mar 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to signal that the content is json. The Application Load Balancer (ALB)/APIGatway api is quite interesting:

  • You send back a json that mimics the properties of an http response. That's why you've properties like headers, statusCode and payload. So the ALB.TargetGroupResponse here is the json equivalent of a HTTPResponse.
  • If you want to send json back to the client you will have a json structure that is your httpresponse (ALB.TargetGroupResponse) and within this structure you will have your actual json payload and headers. Thats why I set http headers here within the json payload.
  • so the outer json payload and http headers (from the actual http response) are there to communicate with the control plane, whereas the inner http headers and payload are communicated back to the user making the request against the apigateway/application load balancer.

Does that make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kinda. since you are receiving headers from the caller, I think we should check that content type is not set before writing on top of it potentially?


extension SNS.Message: DecodableBody {
public var body: String? {
return self.message != "" ? self.message : nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the empty string?

public struct Message {
public enum Attribute {
case string(String)
case binary(ByteBuffer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto re ByteBuffer

try container.encode("Binary", forKey: .dataType)
try container.encode(base64, forKey: .dataValue)
case .string(let string):
try container.encode("String", forKey: .dataType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using const/enum for string/binary?

/// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
public enum Attribute {
case string(String)
case binary(ByteBuffer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto re ByteBuffer

buffer.moveReaderIndex(to: bytes.count)

self = .binary(buffer)
default:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const to enum for String/Number/Binary

data = Data(bytes)
} else {
// TBD: Can this ever fail? I wouldn't think so...
data = payload.data(using: .utf8)!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a guard would be better than force unwrap

values.forEach { value in
nioHeaders.append((key, value))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe can use reduce?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could do that, but i'm afraid that this could lead to some ref count overhead since the array would be copied around quite a lot:

  • we get the target array as input
  • we need to create a var output = input (ref count)
  • we write to the output.append() - still referenced by the input, this means CoW
  • we return output (ref count?!)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe if we make all our headers [String: [String]] this becomes simpler / redundant

Copy link
Contributor

@tomerd tomerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabianfett awesome work, very useful addition. comments and ideas inline, did not review base64 code or tests yet

@fabianfett fabianfett force-pushed the add-lambda-events branch 4 times, most recently from f775b5e to db7f9e6 Compare March 19, 2020 16:31
self.statusDescription = statusDescription
self.headers = headers

var buffer = try encoder.encodeAsByteBuffer(payload, allocator: ByteBufferAllocator())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to find a way to provide an allocator into all such methods

public let resourceId: String
public let apiId: String
public let resourcePath: String
public let httpMethod: String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we are using String and not the nio type. maybe we can do the same in other places, or come up with our own enum?

@fabianfett fabianfett force-pushed the add-lambda-events branch 9 times, most recently from fb799a6 to 0764f2e Compare March 27, 2020 09:52
fixed a stupid bug

Remove bytebuffer usages.
@fabianfett fabianfett closed this Apr 15, 2020
@fabianfett fabianfett deleted the add-lambda-events branch April 23, 2020 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants