Making Swift even safer

Post on 15-Apr-2017

171 views 1 download

Transcript of Making Swift even safer

Making Swift even safer

18 months of development

120k+ lines of Swift

≈40 releases

6 months in production

Juno Rider iOS

Stricter interface

extension Array { public subscript (safe index: Int) -> Element? { … }}

extension Int { public init?(safe value: Float) { … } public init?(safe value: Double) { … } public init?(safe value: UInt) { … }}

!

NonEmptyString

NonEmptyArray

NonNegativeDouble

Stronger types

struct LocationCoordinate<A> { let value: Double init(_ value: Double) { self.value = value }}

enum Lat {}enum Lon {}

struct Location { let latitude: LocationCoordinate<Lat> let longitude: LocationCoordinate<Lon>}

Phantom types

struct TaggedValue<ValueType, Tag> { let value: ValueType init(_ value: ValueType) { self.value = value }}

enum PickupTimeTag {}typealias PickupTime = TaggedValue<NSDate, PickupTimeTag>

enum DropoffTimeTag {}typealias DropoffTime = TaggedValue<NSDate, DropoffTimeTag>

Phantom types

enum PhoneTag {}typealias Phone = TaggedValue<String, PhoneTag>

extension TaggedValueType where Tag == PhoneTag, ValueType == String {

func trimCountryCode() -> Phone { return Phone(trimCountryCode(self.taggedValue.value)) }}

private func trimCountryCode(phone: String) -> String { … }

Phantom types

extension TaggedValueType where ValueType: JSONEncoding {

func encodeJSON() -> AnyObject { return self.taggedValue.value.encodeJSON() }}

Phantom types

enum StringKey { case AccessibilityHomeConfirmButton case DialogInsufficientFundsTitle

...}

func localized(value: Strings) -> String {switch value {case .AccessibilityHomeConfirmButton:

return String.localized(“Accessibility.Home.Confirm.Button”)

...

}}

Static resources

extension HTTP {

enum Error: ErrorType {

case InvalidResponse(request: NSURLRequest, response: NSURLResponse?) case TransportError(request: NSURLRequest, error: NSError) case HTTPError(request: NSURLRequest, response: NSHTTPURLResponse, responseData: NSData?)

case CannotCreateURL(components: NSURLComponents) case InvalidURL(urlString: String) case AuthServiceFailure

case CannotBindStreamPair(request: NSURLRequest) case StreamWriting(request: NSURLRequest, error: NSError?) case StreamGzipEncoding(request: NSURLRequest, operation: HTTP.Error.GzipOperation) }}

Strong ErrorType

extension JSON.Error {

struct Encode: ErrorType { public let error: NSError public let source: Any }

enum Decode: ErrorType { case Unexpected case Serialization(error: NSError, data: NSData) case SchemeMismatch(error: JSON.Error.SchemeMismatch, body: AnyObject?) }

struct SchemeMismatch: ErrorType { public let pathComponents: [String] public let reason: String }}

Strong ErrorType

public enum JSONTaskError: ErrorType { case Task(error: HTTP.Error) case Request(error: JSON.Error.Encode) case Response(response: HTTP.Response, error: JSON.Error.Decode)}

Strong ErrorType

Changing code

Components

Context

App

ContextMay contain dirty things dealing with global state

ContextMay contain dirty things dealing with global state

Unit tests

Contexttypealias AppContext = protocol< StringsServiceContainer, StaticImageServicesContainer, BundleImagesServiceContainer, ReachabilityServiceContainer, AnalyticsServiceContainer, SchedulerContainer, RemoteNotificationsContainer, RemoteNotificationsPermissionContainer, RemoteNotificationClearActionContainer, LocationServiceContainer, ApplicationServiceContainer, DeviceServiceContainer,

…>

class ElDependor: AppContext { … }

class MockContext: AppContext { … }

App

Pure state machine

App

Pure state machine

- Unit tests- Integrated acceptance tests

Acceptance tests via TestAppclass Allow_Rider_to_Have_Max_X_Cards_per_Account_Spec: QuickSpec { override func spec() { var app: TestApp! beforeEach { app = TestApp() } given("rider is entering CC details on Add CC screen") { beforeEach { app.login() app.goToHomeScreen() app.receiveSomePaymentMethods() app.openPayments() app.payments.paymentMethods.tapAddPayment() app.payments.addPayment.enterSomeCC() app.payments.addPayment.tapNext() app.payments.addPayment.enterSomeZipCode() } when("rider taps Add CC button") { beforeEach { app.payments.addPayment.tapDone() } and("BE returns the message about max number of active cards") { beforeEach { app._context.addCreditCard.receive(.Failed(.TooManyPaymentMethods)) }

then("present the Max Cards Added alert") { app.payments.addPayment.expectToPresentAlert() } when("rider taps Ok button") { beforeEach { app.payments.addPayment.alert.tapOK() } then("dismiss the alert") { app.payments.addPayment.expectToNotPresentAlert() }

View: screenshot testing

View: screenshot testing

View: screenshot testing

Detecting & investigating bugs in the field

- smart assertions- diligent logging- daily duty

junoAssert

in Debug - crash 🙀in Release - log.error(), trackNonFatal() and recover 🙏

Logging

switch error { case let .InvalidResponse(value): log.warn( category: logCategory, message: "Unexpected response", payload: [ .RequestIdentifier: error.requestIdentifier, .ResponseIdentifier: error.responseIdentifier, .Description: "\(value.response)", .Path: value.request.path, .URL: value.request.absoluteURL ] )

Logging

Analytics junoAssert ↓

↓log.verbose log.info log.warn log.error ↓ ↓ ↓ ↓

{'key0':'value0','key1':‘value1'} ↓

append to txt file ↓ roll & upload to AWS ↓ Elasticsearch + Kibana

Logging

Production quality comes at a price

- takes up to x2 dev effort- challenging for new team members- performance considerations

But it brings satisfaction

Thank you