Skip to content
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

[OPEN DEV] Add Swiftlint to BraintreeCore #1364

Merged
merged 8 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Lint
on: [pull_request]
concurrency:
group: lint-${{ github.event.number }}
cancel-in-progress: true
jobs:
swiftlint:
name: SwiftLint
runs-on: macOS-14-xlarge
steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Use Xcode 15.0.1
run: sudo xcode-select -switch /Applications/Xcode_15.0.1.app
- name: Install SwiftLint
run: brew install swiftlint
- name: Run SwiftLint
run: swiftlint --strict
106 changes: 106 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Reference: https://github.com/realm/SwiftLint
# Required Swiftlint Version
# swiftlint_version: 0.39.2
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved

# Paths to include in lint
included:
- Sources/BraintreeCore

excluded:
- Sources/BraintreeCore/BTAPIPinnedCertificates.swift

disabled_rules:
- todo
- type_name # tests will have the format <SUT>_Tests
- xctfail_message
- blanket_disable_command
- non_optional_string_data_conversion
- attributes
- multiline_function_chains

opt_in_rules:
- array_init
- closure_end_indentation
- closure_spacing
- collection_alignment
- colon # promote to error
- convenience_type
- discouraged_object_literal
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- fatal_error_message
- first_where
- force_unwrapping
- implicitly_unwrapped_optional
- indentation_width
- last_where
- legacy_random
- literal_expression_end_indentation
- multiline_arguments
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- operator_usage_whitespace
- overridden_super_call
- pattern_matching_keywords
- prefer_self_type_over_type_of_self
- redundant_nil_coalescing
- redundant_type_annotation
- strict_fileprivate
- toggle_bool
- trailing_closure
- unneeded_parentheses_in_closure_argument
- vertical_whitespace_closing_braces
- yoda_condition

custom_rules:
array_constructor:
name: "Array/Dictionary initializer"
regex: '[let,var] .+ = (\[.+\]\(\))'
capture_group: 1
message: "Use explicit type annotation when initializing empty arrays and dictionaries"
severity: warning
space_after_main_type:
name: "No space after main type"
regex: '(class|struct|extension)((?-s)\s.*\{$\n)(?!^\s*$)'
message: "Empty line required after main declarations"
severity: warning

force_cast: warning
force_try: warning
function_body_length:
warning: 60

legacy_hashing: error

identifier_name:
excluded:
- i
- id
- x
- y
- z

indentation_width:
indentation_width: 4

line_length:
warning: 140
ignores_urls: true
ignores_comments: true

multiline_arguments:
first_argument_location: next_line
only_enforce_after_first_closure_on_first_line: true

private_over_fileprivate:
validate_extensions: true

trailing_whitespace:
ignores_empty_lines: true

vertical_whitespace:
max_empty_lines: 2
21 changes: 21 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2237,6 +2237,7 @@
570B9385285397520041BAFE /* Frameworks */,
570B9386285397520041BAFE /* Headers */,
570B93A8285397520041BAFE /* Resources */,
BE676C532C417B8F000A6579 /* Swiftlint */,
);
buildRules = (
);
Expand Down Expand Up @@ -3154,6 +3155,26 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
BE676C532C417B8F000A6579 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "#!/bin/sh\n\nif test -d \"/opt/homebrew/bin/\"; then\n PATH=\"/opt/homebrew/bin/:${PATH}\"\nfi\n\nexport PATH\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
showEnvVarsInLog = 0;
};
CDAB67F3BC5BE564FCFFD6BC /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down
10 changes: 10 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ This document outlines development practices that we follow while developing thi

The included demo app utilizes a [sandbox sample merchant server](https://braintree-sample-merchant.herokuapp.com) hosted on Heroku.

## SwiftLint

Ensure that you have [SwiftLint](https://github.com/realm/SwiftLint) installed as we utilize it within our project.

To install via [Homebrew](https://brew.sh/) run:
```
brew install swiftlint
```
Our Xcode workspace has a `Run Phase` which integrates in `SwiftLint` so the only prerequisite is installing via `Homebrew`.

## Tests

Each module has a corresponding unit test target. These can be run individually, or all at once via the `UnitTests` scheme.
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ Versions 4.9.6 and below use outdated SSL certificates and are unsupported.

## Demo

1. Our Xcode project uses SwiftLint. To ensure you have it installed see [DEVELOPMENT.md](https://github.com/braintree/braintree_ios/blob/main/DEVELOPMENT.md#swiftlint)
1. Run `pod install`
* There is a known M1 mac issue with CocoaPods. See [this solution](https://github.com/CocoaPods/CocoaPods/issues/10220#issuecomment-730963835) to resolve `ffi` dependency issues.
2. Resolve the Swift Package Manager packages if needed: `File` > `Packages` > `Resolve Package Versions` or by running `swift package resolve` in Terminal
3. Open `Braintree.xcworkspace` in Xcode
4. Select the `Demo` scheme, and then run
1. Resolve the Swift Package Manager packages if needed: `File` > `Packages` > `Resolve Package Versions` or by running `swift package resolve` in Terminal
1. Open `Braintree.xcworkspace` in Xcode
1. Select the `Demo` scheme, and then run

Xcode 15.0+ is required to run the demo app.

Expand Down
8 changes: 7 additions & 1 deletion Sources/BraintreeCore/Analytics/BTAnalyticsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ class BTAnalyticsService: Equatable {

// MARK: - Internal Properties

// swiftlint:disable force_unwrapping
/// The FPTI URL to post all analytic events.
static let url = URL(string: "https://api.paypal.com")!
// swiftlint:enable force_unwrapping

/// The HTTP client for communication with the analytics service endpoint. Exposed for testing.
var http: BTHTTP?
Expand Down Expand Up @@ -131,7 +133,11 @@ class BTAnalyticsService: Equatable {
if await !BTAnalyticsService.events.isEmpty {
do {
let configuration = try await apiClient.fetchConfiguration()
let postParameters = await createAnalyticsEvent(config: configuration, sessionID: apiClient.metadata.sessionID, events: Self.events.allValues)
let postParameters = await createAnalyticsEvent(
config: configuration,
sessionID: apiClient.metadata.sessionID,
events: Self.events.allValues
)
http?.post("v1/tracking/batch/events", parameters: postParameters) { _, _, _ in }
await Self.events.removeAll()
} catch {
Expand Down
11 changes: 7 additions & 4 deletions Sources/BraintreeCore/Analytics/FPTIBatchData.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import UIKit

// swiftlint:disable nesting
/// The POST body for a batch upload of FPTI events
struct FPTIBatchData: Codable {

let events: [EventsContainer] // Single-element "events" array required by FPTI formatting

init(metadata: Metadata, events fptiEvents: [Event]?) {
self.events = [EventsContainer(
metadata: metadata,
fptiEvents: fptiEvents ?? []
)]
self.events = [
EventsContainer(
metadata: metadata,
fptiEvents: fptiEvents ?? []
)
]
}

struct EventsContainer: Codable {
Expand Down
8 changes: 3 additions & 5 deletions Sources/BraintreeCore/Authorization/BTClientToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// An authorization string used to initialize the Braintree SDK
@_documentation(visibility: private)
@objcMembers public class BTClientToken: NSObject, NSCoding, NSCopying, ClientAuthorization {

// NEXT_MAJOR_VERSION (v7): properties exposed for Objective-C interoperability + Drop-in access.
// Once the entire SDK is in Swift, determine if we want public properties to be internal and
// what we can make internal without breaking the Drop-in.
Expand Down Expand Up @@ -38,8 +38,7 @@ import Foundation
// Client token must be decoded first because the other values are retrieved from it
self.json = try Self.decodeClientToken(clientToken)

guard let authorizationFingerprint = json["authorizationFingerprint"].asString(),
!authorizationFingerprint.isEmpty else {
guard let authorizationFingerprint = json["authorizationFingerprint"].asString(), !authorizationFingerprint.isEmpty else {
throw BTClientTokenError.invalidAuthorizationFingerprint
}

Expand Down Expand Up @@ -113,8 +112,7 @@ import Foundation
// MARK: - NSObject override

public override func isEqual(_ object: Any?) -> Bool {
guard object is BTClientToken,
let otherToken = object as? BTClientToken else {
guard object is BTClientToken, let otherToken = object as? BTClientToken else {
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public enum BTClientTokenError: Error, CustomNSError, LocalizedError, Equatable
}
}

// swiftlint:disable line_length
public var errorDescription: String? {
switch self {
case .invalidAuthorizationFingerprint:
Expand All @@ -51,4 +52,5 @@ public enum BTClientTokenError: Error, CustomNSError, LocalizedError, Equatable
return "Failed to decode client token. \(description)"
}
}
// swiftlint:enable line_length
}
6 changes: 3 additions & 3 deletions Sources/BraintreeCore/Authorization/TokenizationKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class TokenizationKey: ClientAuthorization {
guard tokenizationKey.range(of: pattern, options: .regularExpression) != nil else { return nil }

let tokenizationKeyParts = tokenizationKey.split(separator: "_", maxSplits: 3)
let environment: String = String(tokenizationKeyParts[0])
let merchantID: String = String(tokenizationKeyParts[2])
let environment = String(tokenizationKeyParts[0])
let merchantID = String(tokenizationKeyParts[2])

var components: URLComponents = URLComponents()
var components = URLComponents()
components.scheme = environment == "development" ? "http" : "https"

guard let host = host(for: environment) else { return nil }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,3 @@ public enum TokenizationKeyError: Int, Error, CustomNSError, LocalizedError, Equ
}
}
}

26 changes: 20 additions & 6 deletions Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation

// swiftlint:disable type_body_length file_length
/// This class acts as the entry point for accessing the Braintree APIs via common HTTP methods performed on API endpoints.
/// - Note: It also manages authentication via tokenization key and provides access to a merchant's gateway configuration.
@objcMembers public class BTAPIClient: NSObject, BTHTTPNetworkTiming {
Expand Down Expand Up @@ -46,7 +47,7 @@ import Foundation
switch authorizationType {
case .tokenizationKey:
do {
self.authorization = try TokenizationKey(authorization)
self.authorization = try TokenizationKey(authorization)
} catch {
return nil
}
Expand All @@ -68,7 +69,7 @@ import Foundation
http?.networkTimingDelegate = self

// Kickoff the background request to fetch the config
fetchOrReturnRemoteConfiguration { configuration, error in
fetchOrReturnRemoteConfiguration { _, _ in
// No-op
}
}
Expand Down Expand Up @@ -148,7 +149,7 @@ import Foundation
"session_id": metadata.sessionID
]

get("v1/payment_methods", parameters: parameters) { body, response, error in
get("v1/payment_methods", parameters: parameters) { body, _, error in
if let error {
completion(nil, error)
return
Expand All @@ -158,7 +159,10 @@ import Foundation

body?["paymentMethods"].asArray()?.forEach { paymentInfo in
let type: String? = paymentInfo["type"].asString()
let paymentMethodNonce: BTPaymentMethodNonce? = BTPaymentMethodNonceParser.shared.parseJSON(paymentInfo, withParsingBlockForType: type)
let paymentMethodNonce: BTPaymentMethodNonce? = BTPaymentMethodNonceParser.shared.parseJSON(
paymentInfo,
withParsingBlockForType: type
)

if let paymentMethodNonce {
paymentMethodNonces.append(paymentMethodNonce)
Expand Down Expand Up @@ -269,7 +273,13 @@ import Foundation
}

let postParameters = BTAPIRequest(requestBody: parameters, metadata: metadata, httpType: httpType)
http(for: httpType)?.post(path, configuration: configuration, parameters: postParameters, headers: headers, completion: completion)
http(for: httpType)?.post(
path,
configuration: configuration,
parameters: postParameters,
headers: headers,
completion: completion
)
}
}

Expand Down Expand Up @@ -339,7 +349,11 @@ import Foundation
let pattern: String = "([a-zA-Z0-9]+)_[a-zA-Z0-9]+_([a-zA-Z0-9_]+)"
guard let regularExpression = try? NSRegularExpression(pattern: pattern) else { return nil }

let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression.firstMatch(in: authorization, options: [], range: NSRange(location: 0, length: authorization.count))
let tokenizationKeyMatch: NSTextCheckingResult? = regularExpression.firstMatch(
in: authorization,
options: [],
range: NSRange(location: 0, length: authorization.count)
)

return tokenizationKeyMatch != nil ? .tokenizationKey : .clientToken
}
Expand Down
Loading
Loading