Skip to content

Commit ff9cc76

Browse files
committed
different approach
1 parent d437b62 commit ff9cc76

File tree

2 files changed

+158
-52
lines changed

2 files changed

+158
-52
lines changed

Sources/AWSLambdaEvents/Cloudwatch.swift

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@ public enum Cloudwatch {
2323
/// **NOTE**: For examples of events that come via CloudWatch Events, see
2424
/// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html
2525
/// https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html
26-
public struct Event: Decodable {
26+
public struct Event<Detail: Decodable>: Decodable {
2727
public let id: String
2828
public let source: String
2929
public let accountId: String
3030
public let time: Date
3131
public let region: AWSRegion
3232
public let resources: [String]
33-
3433
public let detail: Detail
3534

3635
enum CodingKeys: String, CodingKey {
@@ -49,59 +48,63 @@ public enum Cloudwatch {
4948
self.id = try container.decode(String.self, forKey: .id)
5049
self.source = try container.decode(String.self, forKey: .source)
5150
self.accountId = try container.decode(String.self, forKey: .accountId)
52-
let time = try container.decode(ISO8601Coding.self, forKey: .time)
53-
self.time = time.wrappedValue
51+
self.time = (try container.decode(ISO8601Coding.self, forKey: .time)).wrappedValue
5452
self.region = try container.decode(AWSRegion.self, forKey: .region)
5553
self.resources = try container.decode([String].self, forKey: .resources)
56-
57-
self.detail = try Detail(from: decoder)
54+
self.detail = (try DetailCoding(from: decoder)).guts
5855
}
5956

60-
public enum Detail: Decodable {
61-
case scheduled
62-
case ec2InstanceStateChangeNotification(EC2.InstanceStateChangeNotification)
63-
case ec2SpotInstanceInterruptionWarning(EC2.SpotInstanceInterruptionNotice)
64-
case custom(label: String, detail: Decodable)
57+
private struct DetailCoding {
58+
public let guts: Detail
6559

66-
enum CodingKeys: String, CodingKey {
60+
internal enum CodingKeys: String, CodingKey {
6761
case detailType = "detail-type"
6862
case detail
6963
}
7064

71-
// FIXME: make thread safe
72-
static var registry = [String: (Decoder) throws -> Decodable]()
73-
public static func register<T: Decodable>(label: String, type: T.Type) {
74-
registry[label] = type.init
75-
}
76-
7765
public init(from decoder: Decoder) throws {
7866
let container = try decoder.container(keyedBy: CodingKeys.self)
7967
let detailType = try container.decode(String.self, forKey: .detailType)
68+
let detailFactory: (Decoder) throws -> Decodable
8069
switch detailType {
81-
case "Scheduled Event":
82-
self = .scheduled
83-
case "EC2 Instance State-change Notification":
84-
self = .ec2InstanceStateChangeNotification(
85-
try container.decode(EC2.InstanceStateChangeNotification.self, forKey: .detail))
86-
case "EC2 Spot Instance Interruption Warning":
87-
self = .ec2SpotInstanceInterruptionWarning(
88-
try container.decode(EC2.SpotInstanceInterruptionNotice.self, forKey: .detail))
70+
case ScheduledEvent.name:
71+
detailFactory = Empty.init
72+
case EC2.InstanceStateChangeNotificationEvent.name:
73+
detailFactory = EC2.InstanceStateChangeNotification.init
74+
case EC2.SpotInstanceInterruptionNoticeEvent.name:
75+
detailFactory = EC2.SpotInstanceInterruptionNotice.init
8976
default:
90-
guard let factory = Detail.registry[detailType] else {
91-
throw UnknownPayload()
77+
guard let factory = Cloudwatch.detailPayloadRegistry[detailType] else {
78+
throw UnknownPayload(name: detailType)
9279
}
93-
let detailsDecoder = try container.superDecoder(forKey: .detail)
94-
self = .custom(label: detailType, detail: try factory(detailsDecoder))
80+
detailFactory = factory
9581
}
82+
let detailsDecoder = try container.superDecoder(forKey: .detail)
83+
guard let detail = try detailFactory(detailsDecoder) as? Detail else {
84+
throw PayloadTypeMismatch(name: detailType, type: Detail.self)
85+
}
86+
self.guts = detail
9687
}
9788
}
9889
}
9990

100-
public struct CodePipelineStateChange: Decodable {
101-
let foo: String
91+
// MARK: - Detail Payload Registry
92+
93+
// FIXME: make thread safe
94+
private static var detailPayloadRegistry = [String: (Decoder) throws -> Decodable]()
95+
96+
public static func registerDetailPayload<T: Decodable>(label: String, type: T.Type) {
97+
detailPayloadRegistry[label] = type.init
10298
}
10399

100+
// MARK: - Common Event Types
101+
102+
public typealias ScheduledEvent = Event<Empty>
103+
104+
public struct Empty: Decodable {}
105+
104106
public enum EC2 {
107+
public typealias InstanceStateChangeNotificationEvent = Event<InstanceStateChangeNotification>
105108
public struct InstanceStateChangeNotification: Decodable {
106109
public enum State: String, Codable {
107110
case running
@@ -120,6 +123,7 @@ public enum Cloudwatch {
120123
}
121124
}
122125

126+
public typealias SpotInstanceInterruptionNoticeEvent = Event<SpotInstanceInterruptionNotice>
123127
public struct SpotInstanceInterruptionNotice: Decodable {
124128
public enum Action: String, Codable {
125129
case hibernate
@@ -137,5 +141,24 @@ public enum Cloudwatch {
137141
}
138142
}
139143

140-
struct UnknownPayload: Error {}
144+
struct UnknownPayload: Error {
145+
let name: String
146+
}
147+
148+
struct PayloadTypeMismatch: Error {
149+
let name: String
150+
let type: Any
151+
}
152+
}
153+
154+
extension Cloudwatch.ScheduledEvent {
155+
static var name: String { "Scheduled Event" }
156+
}
157+
158+
extension Cloudwatch.EC2.InstanceStateChangeNotificationEvent {
159+
static var name: String { "EC2 Instance State-change Notification" }
160+
}
161+
162+
extension Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent {
163+
static var name: String { "EC2 Spot Instance Interruption Warning" }
141164
}

Tests/AWSLambdaEventsTests/CloudwatchTests.swift

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,97 @@
1616
import XCTest
1717

1818
class CloudwatchTests: XCTestCase {
19-
static let scheduledEventPayload = """
20-
{
21-
"id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
22-
"detail-type": "Scheduled Event",
23-
"source": "aws.events",
24-
"account": "123456789012",
25-
"time": "1970-01-01T00:00:00Z",
26-
"region": "us-east-1",
27-
"resources": [
28-
"arn:aws:events:us-east-1:123456789012:rule/ExampleRule"
29-
],
30-
"detail": {}
19+
static func eventPayload(type: String, details: String) -> String {
20+
"""
21+
{
22+
"id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
23+
"detail-type": "\(type)",
24+
"source": "aws.events",
25+
"account": "123456789012",
26+
"time": "1970-01-01T00:00:00Z",
27+
"region": "us-east-1",
28+
"resources": [
29+
"arn:aws:events:us-east-1:123456789012:rule/ExampleRule"
30+
],
31+
"detail": \(details)
32+
}
33+
"""
3134
}
32-
"""
3335

3436
func testScheduledEventFromJSON() {
35-
let data = CloudwatchTests.scheduledEventPayload.data(using: .utf8)!
36-
var maybeEvent: Cloudwatch.Event?
37-
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.Event.self, from: data))
37+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.ScheduledEvent.name, details: "{}")
38+
let data = payload.data(using: .utf8)!
39+
var maybeEvent: Cloudwatch.ScheduledEvent?
40+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data))
41+
42+
guard let event = maybeEvent else {
43+
return XCTFail("Expected to have an event")
44+
}
45+
46+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
47+
XCTAssertEqual(event.source, "aws.events")
48+
XCTAssertEqual(event.accountId, "123456789012")
49+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
50+
XCTAssertEqual(event.region, .us_east_1)
51+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
52+
}
53+
54+
func testEC2InstanceStateChangeNotificationEventFromJSON() {
55+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.InstanceStateChangeNotificationEvent.name,
56+
details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }")
57+
let data = payload.data(using: .utf8)!
58+
var maybeEvent: Cloudwatch.EC2.InstanceStateChangeNotificationEvent?
59+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.InstanceStateChangeNotificationEvent.self, from: data))
60+
61+
guard let event = maybeEvent else {
62+
return XCTFail("Expected to have an event")
63+
}
64+
65+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
66+
XCTAssertEqual(event.source, "aws.events")
67+
XCTAssertEqual(event.accountId, "123456789012")
68+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
69+
XCTAssertEqual(event.region, .us_east_1)
70+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
71+
XCTAssertEqual(event.detail.instanceId, "0")
72+
XCTAssertEqual(event.detail.state, .stopping)
73+
}
74+
75+
func testEC2SpotInstanceInterruptionNoticeEventFromJSON() {
76+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent.name,
77+
details: "{ \"instance-id\": \"0\", \"instance-action\": \"terminate\" }")
78+
let data = payload.data(using: .utf8)!
79+
var maybeEvent: Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent?
80+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent.self, from: data))
81+
82+
guard let event = maybeEvent else {
83+
return XCTFail("Expected to have an event")
84+
}
85+
86+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
87+
XCTAssertEqual(event.source, "aws.events")
88+
XCTAssertEqual(event.accountId, "123456789012")
89+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
90+
XCTAssertEqual(event.region, .us_east_1)
91+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
92+
XCTAssertEqual(event.detail.instanceId, "0")
93+
XCTAssertEqual(event.detail.action, .terminate)
94+
}
95+
96+
func testCustomEventFromJSON() {
97+
struct Custom: Decodable {
98+
let name: String
99+
}
100+
101+
Cloudwatch.registerDetailPayload(label: "Custom", type: Custom.self)
102+
103+
let payload = CloudwatchTests.eventPayload(type: "Custom", details: "{ \"name\": \"foo\" }")
104+
let data = payload.data(using: .utf8)!
105+
var maybeEvent: Cloudwatch.Event<Custom>?
106+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.Event<Custom>.self, from: data))
38107

39108
guard let event = maybeEvent else {
40-
XCTFail("Expected to have an event"); return
109+
return XCTFail("Expected to have an event")
41110
}
42111

43112
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
@@ -46,9 +115,23 @@ class CloudwatchTests: XCTestCase {
46115
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
47116
XCTAssertEqual(event.region, .us_east_1)
48117
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
118+
XCTAssertEqual(event.detail.name, "foo")
119+
}
120+
121+
func testUnregistredType() {
122+
let payload = CloudwatchTests.eventPayload(type: UUID().uuidString, details: "{}")
123+
let data = payload.data(using: .utf8)!
124+
XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in
125+
XCTAssert(error is Cloudwatch.UnknownPayload, "expected UnknownPayload but recieved \(error)")
126+
}
127+
}
49128

50-
guard case Cloudwatch.Event.Detail.scheduled = event.detail else {
51-
XCTFail("Unexpected detail: \(event.detail)"); return
129+
func testTypeMismatch() {
130+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.InstanceStateChangeNotificationEvent.name,
131+
details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }")
132+
let data = payload.data(using: .utf8)!
133+
XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in
134+
XCTAssert(error is Cloudwatch.PayloadTypeMismatch, "expected PayloadTypeMismatch but recieved \(error)")
52135
}
53136
}
54137
}

0 commit comments

Comments
 (0)