Skip to content

Commit

Permalink
Greater Flexibility (#1)
Browse files Browse the repository at this point in the history
Replaced Header, MIMEType, and RequestMethod enums with structs for greater flexibility.

Opened encoding/decoding methods to public for implementing in additional convenience methods.
  • Loading branch information
richardpiazza authored Apr 15, 2021
1 parent fa2c76e commit b426177
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 108 deletions.
15 changes: 13 additions & 2 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ on:
branches: [ main ]

jobs:
build:
macos-build:

runs-on: macos-latest

steps:
- uses: actions/checkout@v2
- name: Build
- name: Build (macOS)
run: swift build -v
- name: Run tests
run: swift test -v

ubuntu-build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Build (Ubuntu)
run: swift build -v
- name: Run tests
run: swift test -v
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,6 @@ A collection of extensions & wrappers around URLSession.

This package has been designed to work across multiple swift environments by utilizing conditional checks. It has been tested on Apple platforms (macOS, iOS, tvOS, watchOS), as well as Linux (Ubuntu).

## Usage

**SessionPlus** is distributed using the [Swift Package Manager](https://swift.org/package-manager). To install it into a project, add it as a dependency within your `Package.swift` manifest:

```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/richardpiazza/SessionPlus.git", .upToNextMinor(from: "1.0.0")
],
...
)
```

Then import the **SessionPlus** packages wherever you'd like to use it:

```swift
import SessionPlus
```

## Quick Start

Checkout the `WebAPI` class.
Expand Down Expand Up @@ -95,3 +75,24 @@ public protocol HTTPInjectable {
```

The `HTTPInjectable` protocol is used to extend an `HTTPClient` implementation by overriding the default `execute(request:completion:)` implementation to allow for the definition and usage of predefined responses. This makes for simple testing!

## Installation

**SessionPlus** is distributed using the [Swift Package Manager](https://swift.org/package-manager).
To install it into a project, add it as a dependency within your `Package.swift` manifest:

```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/richardpiazza/SessionPlus.git", .upToNextMinor(from: "1.1.0")
],
...
)
```

Then import the **SessionPlus** packages wherever you'd like to use it:

```swift
import SessionPlus
```
48 changes: 48 additions & 0 deletions Sources/SessionPlus/HTTP+Header.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension HTTP {
/// Command HTTP Header
struct Header: ExpressibleByStringLiteral, Equatable {
public let rawValue: String

public init(stringLiteral value: StringLiteralType) {
rawValue = value
}
}
}

extension HTTP.Header: Identifiable {
public var id: String { rawValue }
}

public extension HTTP.Header {
/// HTTP Header date formatter; RFC1123
static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"
formatter.timeZone = TimeZone(identifier: "GMT")!
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}

public extension HTTP.Header {
/// The Accept request HTTP header advertises which content types, expressed as MIME types, the client is able to
/// understand.
static let accept: Self = "Accept"
/// The HTTP Authorization request header contains the credentials to authenticate a user agent with a server,
/// usually after the server has responded with a 401 Unauthorized status and the WWW-Authenticate header.
static let authorization: Self = "Authorization"
/// The Content-Length entity header is indicating the size of the entity-body, in bytes, sent to the recipient.
static let contentLength: Self = "Content-Length"
/// The Content-MD5 header, may be used as a message integrity check (MIC), to verify that the decoded data are the
/// same data that were initially sent.
static let contentMD5: Self = "Content-MD5"
/// The Content-Type entity header is used to indicate the media type of the resource.
static let contentType: Self = "Content-Type"
/// The Date general HTTP header contains the date and time at which the message was originated.
static let date: Self = "Date"
}
49 changes: 49 additions & 0 deletions Sources/SessionPlus/HTTP+MIMEType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension HTTP {
/// MIME Types used in the API
struct MIMEType: ExpressibleByStringLiteral, Equatable {
public let rawValue: String

public init(stringLiteral value: StringLiteralType) {
rawValue = value
}
}
}

extension HTTP.MIMEType: Identifiable {
public var id: String { rawValue }
}

public extension HTTP.MIMEType {
/// Any kind of binary data
static let bin: Self = "application/octet-stream"
/// Graphics Interchange Format (GIF)
static let gif: Self = "image/gif"
/// HyperText Markup Language
static let html: Self = "text/html"
/// JPEG images
static let jpeg: Self = "image/jpeg"
/// JavaScript
static let js: Self = "text/javascript"
/// JSON Document
static let json: Self = "application/json"
/// JSON-LD Document
static let jsonld: Self = "application/ld+json"
/// Portable Network Graphics
static let png: Self = "image/png"
/// Adobe Portable Document Format
static let pdf: Self = "application/pdf"
/// Scalable Vector Graphics
static let svg: Self = "image/svg+xml"
/// Text
static let txt: Self = "text/plain"
/// XML
static let xml: Self = "application/xml"

@available(*, deprecated, renamed: "json")
static var applicationJson: Self { json }
}
37 changes: 37 additions & 0 deletions Sources/SessionPlus/HTTP+RequestMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension HTTP {
/// Desired action to be performed for a given resource.
///
/// Although they can also be nouns, these request methods are sometimes referred as HTTP verbs.
struct RequestMethod: ExpressibleByStringLiteral, Equatable {
public let rawValue: String

public init(stringLiteral value: String) {
rawValue = value
}
}
}

extension HTTP.RequestMethod: Identifiable {
public var id: String { rawValue }
}

public extension HTTP.RequestMethod {
/// The GET method requests a representation of the specified resource.
///
/// Requests using GET should only retrieve data.
static let get: Self = "GET"
/// The PUT method replaces all current representations of the target resource with the request payload.
static let put: Self = "PUT"
/// The POST method is used to submit an entity to the specified resource, often causing a change in state or side
/// effects on the server.
static let post: Self = "POST"
/// The PATCH method is used to apply partial modifications to a resource.
static let patch: Self = "PATCH"
/// The DELETE method deletes the specified resource.
static let delete: Self = "DELETE"
}
71 changes: 4 additions & 67 deletions Sources/SessionPlus/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,9 @@ import FoundationNetworking
/// A Collection of methods/headers/values/types used during basic HTTP interactions.
public struct HTTP {

/// Desired action to be performed for a given resource.
///
/// Although they can also be nouns, these request methods are sometimes referred as HTTP verbs.
public enum RequestMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case patch = "PATCH"
case delete = "DELETE"

public var description: String {
switch self {
case .get:
return "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data."
case .put:
return "The PUT method replaces all current representations of the target resource with the request payload."
case .post:
return "The POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server."
case .patch:
return "The PATCH method is used to apply partial modifications to a resource."
case .delete:
return "The DELETE method deletes the specified resource."
}
}
}

/// HTTP Headers as provided from HTTPURLResponse
public typealias Headers = [AnyHashable : Any]

/// Command HTTP Header
public enum Header: String {
case accept = "Accept"
case authorization = "Authorization"
case contentLength = "Content-Length"
case contentMD5 = "Content-MD5"
case contentType = "Content-Type"
case date = "Date"

public var description: String {
switch self {
case .accept:
return "The Accept request HTTP header advertises which content types, expressed as MIME types, the client is able to understand."
case .authorization:
return "The HTTP Authorization request header contains the credentials to authenticate a user agent with a server, usually after the server has responded with a 401 Unauthorized status and the WWW-Authenticate header."
case .contentLength:
return "The Content-Length entity header is indicating the size of the entity-body, in bytes, sent to the recipient."
case .contentType:
return "The Content-Type entity header is used to indicate the media type of the resource."
case .contentMD5:
return "The Content-MD5 header, may be used as a message integrity check (MIC), to verify that the decoded data are the same data that were initially sent."
case .date:
return "The Date general HTTP header contains the date and time at which the message was originated."
}
}

/// HTTP Header date formatter; RFC1123
public static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"
formatter.timeZone = TimeZone(identifier: "GMT")!
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}

/// MIME Types used in the API
public enum MIMEType: String {
case applicationJson = "application/json"
}

/// Authorization schemes used in the API
public enum Authorization {
case basic(username: String, password: String?)
Expand Down Expand Up @@ -127,4 +60,8 @@ public extension URLRequest {
mutating func setValue(_ value: String, forHTTPHeader header: HTTP.Header) {
self.setValue(value, forHTTPHeaderField: header.rawValue)
}

mutating func setValue(_ value: HTTP.MIMEType, forHTTPHeader header: HTTP.Header) {
self.setValue(value.rawValue, forHTTPHeaderField: header.rawValue)
}
}
4 changes: 2 additions & 2 deletions Sources/SessionPlus/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public extension HTTPClient {
request.setValue("\(data.count)", forHTTPHeader: HTTP.Header.contentLength)
}
request.setValue(HTTP.Header.dateFormatter.string(from: Date()), forHTTPHeader: HTTP.Header.date)
request.setValue(HTTP.MIMEType.applicationJson.rawValue, forHTTPHeader: HTTP.Header.accept)
request.setValue(HTTP.MIMEType.applicationJson.rawValue, forHTTPHeader: HTTP.Header.contentType)
request.setValue(HTTP.MIMEType.json, forHTTPHeader: HTTP.Header.accept)
request.setValue(HTTP.MIMEType.json, forHTTPHeader: HTTP.Header.contentType)

if let authorization = self.authorization {
request.setValue(authorization.headerValue, forHTTPHeader: HTTP.Header.authorization)
Expand Down
4 changes: 2 additions & 2 deletions Sources/SessionPlus/HTTPCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ public protocol HTTPCodable {
}

public extension HTTPCodable where Self: HTTPClient {
fileprivate func encode<E: Encodable>(_ encodable: E?) throws -> Data? {
func encode<E: Encodable>(_ encodable: E?) throws -> Data? {
var data: Data? = nil
if let encodable = encodable {
data = try jsonEncoder.encode(encodable)
}
return data
}

fileprivate func decode<D: Decodable>(statusCode: Int, headers: HTTP.Headers?, data: Data?, error: Swift.Error?, completion: @escaping HTTP.CodableTaskCompletion<D>) {
func decode<D: Decodable>(statusCode: Int, headers: HTTP.Headers?, data: Data?, error: Swift.Error?, completion: @escaping HTTP.CodableTaskCompletion<D>) {
guard let data = data else {
completion(statusCode, headers, nil, error)
return
Expand Down
15 changes: 5 additions & 10 deletions Sources/SessionPlus/HTTPInjectable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,17 @@ public struct InjectedPath: Hashable {
var absolutePath: String

public init(request: URLRequest) {
var m = HTTP.RequestMethod.get
if let httpMethod = request.httpMethod, let requestMethod = HTTP.RequestMethod(rawValue: httpMethod) {
m = requestMethod
}
var a = ""
if let url = request.url {
a = url.absoluteString
}
self.init(method: m, absolutePath: a)
let method = HTTP.RequestMethod(stringLiteral: request.httpMethod ?? HTTP.RequestMethod.get.rawValue)
let path = request.url?.absoluteString ?? ""
self.init(method: method, absolutePath: path)
}

@available(*, deprecated, renamed: "init(method:absolutePath:)")
public init(string: String) {
self.init(method: .get, absolutePath: string)
}

public init(method: HTTP.RequestMethod, absolutePath: String) {
public init(method: HTTP.RequestMethod = .get, absolutePath: String) {
self.method = method
self.absolutePath = absolutePath
}
Expand Down
13 changes: 8 additions & 5 deletions Tests/SessionPlusTests/WebAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class WebAPITests: XCTestCase {
}

let injectedResponse = InjectedResponse(statusCode: 200, headers: nil, data: data, error: nil, timeout: 2)
webApi.injectedResponses[InjectedPath(string: "http://www.example.com/api/test")] = injectedResponse
webApi.injectedResponses[InjectedPath(absolutePath: "http://www.example.com/api/test")] = injectedResponse
}

func testInjectedResponse() {
Expand Down Expand Up @@ -71,22 +71,25 @@ class WebAPITests: XCTestCase {
}

func testIPv6DNSError() {
#if canImport(ObjectiveC)
// Temporarily disabled until debugging on Linux can be done.
let expectation = self.expectation(description: "IPv6 DNS Error")

let invalidApi = WebAPI(baseURL: URL(string: "https://api.richardpiazza.com")!)
invalidApi.get("") { (statusCode, response, responseObject, error) in
guard let _ = error else {
XCTFail()
guard error != nil else {
XCTFail("Did not receive expected error.")
return
}

expectation.fulfill()
}

waitForExpectations(timeout: 10) { (error) in
if let _ = error {
XCTFail()
if let e = error {
XCTFail(e.localizedDescription)
}
}
#endif
}
}

0 comments on commit b426177

Please sign in to comment.