Merge pull request #27 from writefreely/refactor-wfclient-with-templates

Refactor WFClient with Templates
This commit is contained in:
Angelo Stavrow 2021-05-20 16:19:29 -04:00 committed by GitHub
commit a01bad4a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 943 additions and 922 deletions

View File

@ -0,0 +1,18 @@
# Types
- [WFClient](/WFClient)
- [WFError](/WFError)
- [WFCollection](/WFCollection)
- [WFPost](/WFPost)
- [WFUser](/WFUser)
# Protocols
- [URLSessionProtocol](/URLSessionProtocol):
Define requirements for `URLSession`s here for dependency-injection purposes (specifically, for testing).
- [URLSessionDataTaskProtocol](/URLSessionDataTaskProtocol):
Define requirements for `URLSessionDataTask`s here for dependency-injection purposes (specifically, for testing).
# Extensions
- [URLSession](/URLSession)

View File

@ -0,0 +1,12 @@
# Extensions on URLSession
## Methods
### `dataTask(with:completionHandler:)`
``` swift
public func dataTask(
with request: URLRequest,
completionHandler: @escaping DataTaskResult
) -> URLSessionDataTaskProtocol
```

View File

@ -0,0 +1,15 @@
# URLSessionDataTaskProtocol
Define requirements for `URLSessionDataTask`s here for dependency-injection purposes (specifically, for testing).
``` swift
public protocol URLSessionDataTaskProtocol
```
## Requirements
### resume()
``` swift
func resume()
```

View File

@ -0,0 +1,21 @@
# URLSessionProtocol
Define requirements for `URLSession`s here for dependency-injection purposes (specifically, for testing).
``` swift
public protocol URLSessionProtocol
```
## Requirements
### DataTaskResult
``` swift
typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void
```
### dataTask(with:completionHandler:)
``` swift
func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol
```

View File

@ -0,0 +1,373 @@
# WFClient
``` swift
public class WFClient
```
## Initializers
### `init(for:with:)`
Initializes the WriteFreely client.
``` swift
public init(for instanceURL: URL, with session: URLSessionProtocol = URLSession.shared)
```
Required for connecting to the API endpoints of a WriteFreely instance.
#### Parameters
- instanceURL: The URL for the WriteFreely instance to which we're connecting, including the protocol.
- session: The URL session to use for connections; defaults to `URLSession.shared`.
## Properties
### `requestURL`
``` swift
public var requestURL: URL
```
### `user`
``` swift
public var user: WFUser?
```
## Methods
### `createCollection(token:withTitle:alias:completion:)`
Creates a new collection.
``` swift
public func createCollection(
token: String? = nil,
withTitle title: String,
alias: String? = nil,
completion: @escaping (Result<WFCollection, Error>) -> Void
)
```
If only a `title` is given, the server will generate and return an alias; in this case, clients should store
the returned `alias` for future operations.
#### Parameters
- token: The access token for the user creating the collection.
- title: The title of the new collection.
- alias: The alias of the collection.
- completion: A handler for the returned `WFCollection` on success, or `Error` on failure.
### `getCollection(token:withAlias:completion:)`
Retrieves a collection's metadata.
``` swift
public func getCollection(
token: String? = nil,
withAlias alias: String,
completion: @escaping (Result<WFCollection, Error>) -> Void
)
```
Collections can be retrieved without authentication. However, authentication is required for retrieving a
private collection or one with scheduled posts.
#### Parameters
- token: The access token for the user retrieving the collection.
- alias: The alias for the collection to be retrieved.
- completion: A handler for the returned `WFCollection` on success, or `Error` on failure.
### `deleteCollection(token:withAlias:completion:)`
Permanently deletes a collection.
``` swift
public func deleteCollection(
token: String? = nil,
withAlias alias: String,
completion: @escaping (Result<Bool, Error>) -> Void
)
```
Any posts in the collection are not deleted; rather, they are made anonymous.
#### Parameters
- token: The access token for the user deleting the collection.
- alias: The alias for the collection to be deleted.
- completion: A hander for the returned `Bool` on success, or `Error` on failure.
### `getPosts(token:in:completion:)`
Retrieves an array of posts.
``` swift
public func getPosts(
token: String? = nil,
in collectionAlias: String? = nil,
completion: @escaping (Result<[WFPost], Error>) -> Void
)
```
If the `collectionAlias` argument is provided, an array of all posts in that collection is retrieved; if
omitted, an array of all posts created by the user whose access token is provided is retrieved.
Collection posts can be retrieved without authentication; however, authentication is required for retrieving a
private collection or one with scheduled posts.
#### Parameters
- token: The access token for the user retrieving the posts.
- collectionAlias: The alias for the collection whose posts are to be retrieved.
- completion: A handler for the returned `[WFPost]` on success, or `Error` on failure.
### `movePost(token:postId:with:to:completion:)`
Moves a post to a collection.
``` swift
public func movePost(
token: String? = nil,
postId: String,
with modifyToken: String? = nil,
to collectionAlias: String?,
completion: @escaping (Result<Bool, Error>) -> Void
)
```
>
#### Parameters
- token: The access token for the user moving the post to a collection.
- postId: The ID of the post to add to the collection.
- modifyToken: The post's modify token; required if the post doesn't belong to the requesting user. If `collectionAlias` is `nil`, do not include a `modifyToken`.
- collectionAlias: The alias of the collection to which the post should be added; if `nil`, this removes the post from any collection.
- completion: A handler for the returned `Bool` on success, or `Error` on failure.
### `pinPost(token:postId:at:in:completion:)`
Pins a post to a collection.
``` swift
public func pinPost(
token: String? = nil,
postId: String,
at position: Int? = nil,
in collectionAlias: String,
completion: @escaping (Result<Bool, Error>) -> Void
)
```
Pinning a post to a collection adds it as a navigation item in the collection/blog home page header, rather
than on the blog itself. While the API endpoint can take an array of posts, this function only accepts a single
post.
#### Parameters
- token: The access token of the user pinning the post to the collection.
- postId: The ID of the post to be pinned.
- position: The numeric position in which to pin the post; if `nil`, will pin at the end of the list.
- collectionAlias: The alias of the collection to which the post should be pinned.
- completion: A handler for the `Bool` returned on success, or `Error` on failure.
### `unpinPost(token:postId:from:completion:)`
Unpins a post from a collection.
``` swift
public func unpinPost(
token: String? = nil,
postId: String,
from collectionAlias: String,
completion: @escaping (Result<Bool, Error>) -> Void
)
```
Removes the post from a navigation item and puts it back on the blog itself. While the API endpoint can take an
array of posts, this function only accepts a single post.
#### Parameters
- token: The access token of the user un-pinning the post from the collection.
- postId: The ID of the post to be un-pinned.
- collectionAlias: The alias of the collection to which the post should be un-pinned.
- completion: A handler for the `Bool` returned on success, or `Error` on failure.
### `createPost(token:post:in:completion:)`
Creates a new post.
``` swift
public func createPost(
token: String? = nil,
post: WFPost,
in collectionAlias: String? = nil,
completion: @escaping (Result<WFPost, Error>) -> Void
)
```
Creates a new post. If a `collectionAlias` is provided, the post is published to that collection; otherwise, it
is posted to the user's Drafts.
#### Parameters
- token: The access token of the user creating the post.
- post: The `WFPost` object to be published.
- collectionAlias: The collection to which the post should be published.
- completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `getPost(token:byId:completion:)`
Retrieves a post.
``` swift
public func getPost(
token: String? = nil,
byId postId: String,
completion: @escaping (Result<WFPost, Error>) -> Void
)
```
The `WFPost` object returned may include additional data, including page views and extracted tags.
#### Parameters
- token: The access token of the user retrieving the post.
- postId: The ID of the post to be retrieved.
- completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `getPost(token:bySlug:from:completion:)`
Retrieves a post from a collection.
``` swift
public func getPost(
token: String? = nil,
bySlug slug: String,
from collectionAlias: String,
completion: @escaping (Result<WFPost, Error>) -> Void
)
```
Collection posts can be retrieved without authentication. However, authentication is required for retrieving a
post from a private collection.
The `WFPost` object returned may include additional data, including page views and extracted tags.
#### Parameters
- token: The access token of the user retrieving the post.
- slug: The slug of the post to be retrieved.
- collectionAlias: The alias of the collection from which the post should be retrieved.
- completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `updatePost(token:postId:updatedPost:with:completion:)`
Updates an existing post.
``` swift
public func updatePost(
token: String? = nil,
postId: String,
updatedPost: WFPost,
with modifyToken: String? = nil,
completion: @escaping (Result<WFPost, Error>) -> Void
)
```
Note that if the `updatedPost` object is provided without a title, the original post's title will be removed.
>
#### Parameters
- token: The access token for the user updating the post.
- postId: The ID of the post to be updated.
- updatedPost: The `WFPost` object with which to update the existing post.
- modifyToken: The post's modify token; required if the post doesn't belong to the requesting user.
- completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `deletePost(token:postId:with:completion:)`
Deletes an existing post.
``` swift
public func deletePost(
token: String? = nil,
postId: String,
with modifyToken: String? = nil,
completion: @escaping (Result<Bool, Error>) -> Void
)
```
>
#### Parameters
- token: The access token for the user deleting the post.
- postId: The ID of the post to be deleted.
- modifyToken: The post's modify token; required if the post doesn't belong to the requesting user.
- completion: A handler for the `Bool` object returned on success, or `Error` on failure.
### `login(username:password:completion:)`
Logs the user in to their account on the WriteFreely instance.
``` swift
public func login(username: String, password: String, completion: @escaping (Result<WFUser, Error>) -> Void)
```
On successful login, the `WFClient`'s `user` property is set to the returned `WFUser` object; this allows
authenticated requests to be made without having to provide an access token.
It is otherwise not necessary to login the user if their access token is provided to the calling function.
#### Parameters
- username: The user's username.
- password: The user's password.
- completion: A handler for the `WFUser` object returned on success, or `Error` on failure.
### `logout(token:completion:)`
Invalidates the user's access token.
``` swift
public func logout(token: String? = nil, completion: @escaping (Result<Bool, Error>) -> Void)
```
#### Parameters
- token: The token to invalidate.
- completion: A handler for the `Bool` object returned on success, or `Error` on failure.
### `getUserData(token:completion:)`
Retrieves a user's basic data.
``` swift
public func getUserData(token: String? = nil, completion: @escaping (Result<Data, Error>) -> Void)
```
#### Parameters
- token: The access token for the user to fetch.
- completion: A handler for the `Data` object returned on success, or `Error` on failure.
### `getUserCollections(token:completion:)`
Retrieves a user's collections.
``` swift
public func getUserCollections(token: String? = nil, completion: @escaping (Result<[WFCollection], Error>) -> Void)
```
#### Parameters
- token: The access token for the user whose collections are to be retrieved.
- completion: A handler for the `[WFCollection]` object returned on success, or `Error` on failure.

View File

@ -1,7 +1,7 @@
# WFCollection # WFCollection
``` swift ``` swift
public struct WFCollection public struct WFCollection
``` ```
## Inheritance ## Inheritance
@ -15,7 +15,7 @@ public struct WFCollection
Creates a basic `WFCollection` object. Creates a basic `WFCollection` object.
``` swift ``` swift
public init(title: String, alias: String?) public init(title: String, alias: String?)
``` ```
This initializer creates a bare-minimum `WFCollection` object for sending to the server; use the decoder-based This initializer creates a bare-minimum `WFCollection` object for sending to the server; use the decoder-based
@ -25,22 +25,22 @@ If no `alias` parameter is provided, one will be generated by the server.
#### Parameters #### Parameters
- title: - title: The title to give the Collection. - title: The title to give the Collection.
- alias: - alias: The alias for the Collection. - alias: The alias for the Collection.
### `init(from:)` ### `init(from:)`
Creates a `WFCollection` object from the server response. Creates a `WFCollection` object from the server response.
``` swift ``` swift
public init(from decoder: Decoder) throws public init(from decoder: Decoder) throws
``` ```
Primarily used by the `WFClient` to create a `WFCollection` object from the JSON returned by the server. Primarily used by the `WFClient` to create a `WFCollection` object from the JSON returned by the server.
#### Parameters #### Parameters
- decoder: - decoder: The decoder to use for translating the server response to a Swift object. - decoder: The decoder to use for translating the server response to a Swift object.
#### Throws #### Throws
@ -51,47 +51,47 @@ Error thrown by the `try` attempt when decoding any given property.
### `alias` ### `alias`
``` swift ``` swift
var alias: String? public var alias: String?
``` ```
### `title` ### `title`
``` swift ``` swift
var title: String public var title: String
``` ```
### `description` ### `description`
``` swift ``` swift
var description: String? public var description: String?
``` ```
### `styleSheet` ### `styleSheet`
``` swift ``` swift
var styleSheet: String? public var styleSheet: String?
``` ```
### `isPublic` ### `isPublic`
``` swift ``` swift
var isPublic: Bool? public var isPublic: Bool?
``` ```
### `views` ### `views`
``` swift ``` swift
var views: Int? public var views: Int?
``` ```
### `email` ### `email`
``` swift ``` swift
var email: String? public var email: String?
``` ```
### `url` ### `url`
``` swift ``` swift
var url: String? public var url: String?
``` ```

View File

@ -0,0 +1,101 @@
# WFError
``` swift
public enum WFError: Int, Error
```
## Inheritance
`Error`, `Int`
## Enumeration Cases
### `badRequest`
``` swift
case badRequest = 400
```
### `unauthorized`
``` swift
case unauthorized = 401
```
### `forbidden`
``` swift
case forbidden = 403
```
### `notFound`
``` swift
case notFound = 404
```
### `methodNotAllowed`
``` swift
case methodNotAllowed = 405
```
### `gone`
``` swift
case gone = 410
```
### `preconditionFailed`
``` swift
case preconditionFailed = 412
```
### `tooManyRequests`
``` swift
case tooManyRequests = 429
```
### `internalServerError`
``` swift
case internalServerError = 500
```
### `badGateway`
``` swift
case badGateway = 502
```
### `serviceUnavailable`
``` swift
case serviceUnavailable = 503
```
### `unknownError`
``` swift
case unknownError = -1
```
### `couldNotComplete`
``` swift
case couldNotComplete = -2
```
### `invalidResponse`
``` swift
case invalidResponse = -3
```
### `invalidData`
``` swift
case invalidData = -4
```

51
docs/WFPost.md → .build/documentation/WFPost.md Executable file → Normal file
View File

@ -1,7 +1,7 @@
# WFPost # WFPost
``` swift ``` swift
public struct WFPost public struct WFPost
``` ```
## Inheritance ## Inheritance
@ -15,7 +15,14 @@ public struct WFPost
Creates a basic `WFPost` object. Creates a basic `WFPost` object.
``` swift ``` swift
public init(body: String, title: String? = nil, appearance: String? = nil, language: String? = nil, rtl: Bool? = nil, createdDate: Date? = nil) public init(
body: String,
title: String? = nil,
appearance: String? = nil,
language: String? = nil,
rtl: Bool? = nil,
createdDate: Date? = nil
)
``` ```
This initializer creates a bare-minimum `WFPost` object for sending to the server; use the decoder-based This initializer creates a bare-minimum `WFPost` object for sending to the server; use the decoder-based
@ -26,26 +33,26 @@ the server.
#### Parameters #### Parameters
- body: - body: The body text for the post. - body: The body text for the post.
- title: - title: The title for the post. - title: The title for the post.
- appearance: - appearance: The appearance for the post; one of `sans`, `serif`/`norm`, `wrap`, `mono`, or `code`. Defaults to `serif`. - appearance: The appearance for the post; one of `sans`, `serif`/`norm`, `wrap`, `mono`, or `code`. Defaults to `serif`.
- language: - language: An ISO 639-1 language code. - language: An ISO 639-1 language code.
- rtl: - rtl: Set to `true` to show content right-to-left. - rtl: Set to `true` to show content right-to-left.
- createdDate: - createdDate: The published date for the post. - createdDate: The published date for the post.
### `init(from:)` ### `init(from:)`
Creates a `WFPost` object from the server response. Creates a `WFPost` object from the server response.
``` swift ``` swift
public init(from decoder: Decoder) throws public init(from decoder: Decoder) throws
``` ```
Primarily used by the `WFClient` to create a `WFPost` object from the JSON returned by the server. Primarily used by the `WFClient` to create a `WFPost` object from the JSON returned by the server.
#### Parameters #### Parameters
- decoder: - decoder: The decoder to use for translating the server response to a Swift object. - decoder: The decoder to use for translating the server response to a Swift object.
#### Throws #### Throws
@ -56,71 +63,71 @@ Error thrown by the `try` attempt when decoding any given property.
### `postId` ### `postId`
``` swift ``` swift
var postId: String? public var postId: String?
``` ```
### `slug` ### `slug`
``` swift ``` swift
var slug: String? public var slug: String?
``` ```
### `appearance` ### `appearance`
``` swift ``` swift
var appearance: String? public var appearance: String?
``` ```
### `language` ### `language`
``` swift ``` swift
var language: String? public var language: String?
``` ```
### `rtl` ### `rtl`
``` swift ``` swift
var rtl: Bool? public var rtl: Bool?
``` ```
### `createdDate` ### `createdDate`
``` swift ``` swift
var createdDate: Date? public var createdDate: Date?
``` ```
### `updatedDate` ### `updatedDate`
``` swift ``` swift
var updatedDate: Date? public var updatedDate: Date?
``` ```
### `title` ### `title`
``` swift ``` swift
var title: String? public var title: String?
``` ```
### `body` ### `body`
``` swift ``` swift
var body: String public var body: String
``` ```
### `tags` ### `tags`
``` swift ``` swift
var tags: [String]? public var tags: [String]?
``` ```
### `views` ### `views`
``` swift ``` swift
var views: Int? public var views: Int?
``` ```
### `collectionAlias` ### `collectionAlias`
``` swift ``` swift
var collectionAlias: String? public var collectionAlias: String?
``` ```

20
docs/WFUser.md → .build/documentation/WFUser.md Executable file → Normal file
View File

@ -1,7 +1,7 @@
# WFUser # WFUser
``` swift ``` swift
public struct WFUser public struct WFUser
``` ```
## Inheritance ## Inheritance
@ -15,29 +15,29 @@ public struct WFUser
Creates a minimum `WFUser` object from a stored token. Creates a minimum `WFUser` object from a stored token.
``` swift ``` swift
public init(token: String, username: String?) public init(token: String, username: String?)
``` ```
Use this when the client has already logged in a user and only needs to reconstruct the type from saved properties. Use this when the client has already logged in a user and only needs to reconstruct the type from saved properties.
#### Parameters #### Parameters
- token: - token: The user's access token - token: The user's access token
- username: - username: The user's username (optional) - username: The user's username (optional)
### `init(from:)` ### `init(from:)`
Creates a `WFUser` object from the server response. Creates a `WFUser` object from the server response.
``` swift ``` swift
public init(from decoder: Decoder) throws public init(from decoder: Decoder) throws
``` ```
Primarily used by the `WFClient` to create a `WFUser` object from the JSON returned by the server. Primarily used by the `WFClient` to create a `WFUser` object from the JSON returned by the server.
#### Parameters #### Parameters
- decoder: - decoder: The decoder to use for translating the server response to a Swift object. - decoder: The decoder to use for translating the server response to a Swift object.
#### Throws #### Throws
@ -48,23 +48,23 @@ Error thrown by the `try` attempt when decoding any given property.
### `token` ### `token`
``` swift ``` swift
var token: String public var token: String
``` ```
### `username` ### `username`
``` swift ``` swift
var username: String? public var username: String?
``` ```
### `email` ### `email`
``` swift ``` swift
var email: String? public var email: String?
``` ```
### `createdDate` ### `createdDate`
``` swift ``` swift
var createdDate: Date? public var createdDate: Date?
``` ```

View File

@ -0,0 +1 @@
Generated at 2021-05-20T15:56:24-0400 using [swift-doc](https://github.com/SwiftDocOrg/swift-doc) 1.0.0-beta.6.

View File

@ -0,0 +1,25 @@
<details>
<summary>Types</summary>
- [WFClient](/WFClient)
- [WFCollection](/WFCollection)
- [WFError](/WFError)
- [WFPost](/WFPost)
- [WFUser](/WFUser)
</details>
<details>
<summary>Protocols</summary>
- [URLSessionDataTaskProtocol](/URLSessionDataTaskProtocol)
- [URLSessionProtocol](/URLSessionProtocol)
</details>
<details>
<summary>Extensions</summary>
- [URLSession](/URLSession)
</details>

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
.DS_Store .DS_Store
/.build
/Packages /Packages
/*.xcodeproj /*.xcodeproj
xcuserdata/ xcuserdata/
!/.build/
/.build/*
!/.build/documentation/

View File

@ -13,8 +13,8 @@ You'll need Xcode 11.5 / Swift 5.2 installed along with the command line tools t
Additionally, documentation is generated by [SwiftDoc](https://github.com/SwiftDocOrg/swift-doc). After making any changes to the package's public API, you'll need to regenerate the docs; to do so, run the following commands in the terminal from the root directory of the package: Additionally, documentation is generated by [SwiftDoc](https://github.com/SwiftDocOrg/swift-doc). After making any changes to the package's public API, you'll need to regenerate the docs; to do so, run the following commands in the terminal from the root directory of the package:
```bash ```bash
$ rm -rf docs/ $ rm -rf .build/documentation
$ swift doc generate Sources --module-name WriteFreely --output docs $ swift doc generate Sources --module-name WriteFreely
``` ```
### Installing ### Installing

View File

@ -0,0 +1,36 @@
import Foundation
struct ServerData<T: Decodable>: Decodable {
enum CodingKeys: String, CodingKey {
case code
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
code = try container.decode(Int.self, forKey: .code)
data = try container.decode(T.self, forKey: .data)
}
let code: Int
let data: T
}
struct NestedPostsJson: Decodable {
enum CodingKeys: String, CodingKey {
case code
case data
enum PostKeys: String, CodingKey {
case posts
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let postsContainer = try container.nestedContainer(keyedBy: CodingKeys.PostKeys.self, forKey: .data)
data = try postsContainer.decode([WFPost].self, forKey: .posts)
}
let data: [WFPost]
}

View File

@ -0,0 +1,114 @@
import Foundation
extension WFClient {
/// Sends a `GET` request.
/// - Parameters:
/// - request: The `URLRequest` for the `GET` request
/// - completion: A closure that captures a `Result` with a `Data` object on success, or a `WFError` on failure.
func get(with request: URLRequest, completion: @escaping (Result<Data, WFError>) -> Void) {
if request.httpMethod != "GET" {
preconditionFailure("Expected GET request, but got \(request.httpMethod ?? "nil")")
}
let dataTask = session.dataTask(with: request) { (data, response, error) in
if error != nil {
completion(.failure(.couldNotComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
if let response = response as? HTTPURLResponse {
completion(.failure(WFError(rawValue: response.statusCode) ?? .invalidResponse))
} else {
completion(.failure(.invalidResponse))
}
return
}
guard let data = data else {
completion(.failure(.invalidData))
return
}
completion(.success(data))
}
dataTask.resume()
}
/// Sends a `POST` request.
/// - Parameters:
/// - request: The `URLRequest` for the `POST` request
/// - expecting: The status code expected to be returned by the server
/// - completion: A closure that captures a `Result` with a `Data` object on success, or a `WFError` on failure.
func post(
with request: URLRequest,
expecting statusCode: Int,
completion: @escaping (Result<Data, WFError>) -> Void
) {
if request.httpMethod != "POST" {
preconditionFailure("Expected POST request, but got \(request.httpMethod ?? "nil")")
}
let dataTask = session.dataTask(with: request) { (data, response, error) in
if error != nil {
completion(.failure(.couldNotComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == statusCode else {
if let response = response as? HTTPURLResponse {
completion(.failure(WFError(rawValue: response.statusCode) ?? .invalidResponse))
} else {
completion(.failure(.invalidResponse))
}
return
}
guard let data = data else {
completion(.failure(.invalidData))
return
}
completion(.success(data))
}
dataTask.resume()
}
/// Sends a `DELETE` request.
/// - Parameters:
/// - request: The `URLRequest` for the `DELETE` request
/// - completion: A closure that captures a `Result` with a `Data` object on success, or a `WFError` on failure.
func delete(with request: URLRequest, completion: @escaping (Result<Data, WFError>) -> Void) {
if request.httpMethod != "DELETE" {
preconditionFailure("Expected DELETE request, but got \(request.httpMethod ?? "nil")")
}
let dataTask = session.dataTask(with: request) { (data, response, error) in
if error != nil {
completion(.failure(.couldNotComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 204 else {
if let response = response as? HTTPURLResponse {
completion(.failure(WFError(rawValue: response.statusCode) ?? .invalidResponse))
} else {
completion(.failure(.invalidResponse))
}
return
}
guard let data = data else {
completion(.failure(.invalidData))
return
}
completion(.success(data))
}
dataTask.resume()
}
}

View File

@ -1,42 +1,23 @@
import Foundation import Foundation
struct ServerData<T: Decodable>: Decodable { // MARK: - URLSession-related protocols
enum CodingKeys: String, CodingKey {
case code
case data
}
init(from decoder: Decoder) throws { /// Define requirements for `URLSession`s here for dependency-injection purposes (specifically, for testing).
let container = try decoder.container(keyedBy: CodingKeys.self) public protocol URLSessionProtocol {
code = try container.decode(Int.self, forKey: .code) typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void
data = try container.decode(T.self, forKey: .data) func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol
}
let code: Int
let data: T
} }
struct NestedPostsJson: Decodable { /// Define requirements for `URLSessionDataTask`s here for dependency-injection purposes (specifically, for testing).
enum CodingKeys: String, CodingKey { public protocol URLSessionDataTaskProtocol {
case code func resume()
case data
enum PostKeys: String, CodingKey {
case posts
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let postsContainer = try container.nestedContainer(keyedBy: CodingKeys.PostKeys.self, forKey: .data)
data = try postsContainer.decode([WFPost].self, forKey: .posts)
}
let data: [WFPost]
} }
// MARK: - Class definition
public class WFClient { public class WFClient {
let decoder = JSONDecoder() let decoder: JSONDecoder
let session: URLSessionProtocol
public var requestURL: URL public var requestURL: URL
public var user: WFUser? public var user: WFUser?
@ -45,11 +26,17 @@ public class WFClient {
/// ///
/// Required for connecting to the API endpoints of a WriteFreely instance. /// Required for connecting to the API endpoints of a WriteFreely instance.
/// ///
/// - Parameter instanceURL: The URL for the WriteFreely instance to which we're connecting, including the protocol. /// - Parameters:
public init(for instanceURL: URL) { /// - instanceURL: The URL for the WriteFreely instance to which we're connecting, including the protocol.
/// - session: The URL session to use for connections; defaults to `URLSession.shared`.
public init(for instanceURL: URL, with session: URLSessionProtocol = URLSession.shared) {
decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
self.session = session
// TODO: - Check that the protocol for instanceURL is HTTPS // TODO: - Check that the protocol for instanceURL is HTTPS
requestURL = URL(string: "api/", relativeTo: instanceURL) ?? instanceURL requestURL = URL(string: "api/", relativeTo: instanceURL) ?? instanceURL
decoder.dateDecodingStrategy = .iso8601
} }
// MARK: - Collection-related methods // MARK: - Collection-related methods
@ -98,33 +85,19 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in post(with: request, expecting: 201) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let collection = try self.decoder.decode(ServerData<WFCollection>.self, from: data)
completion(.success(collection.data))
if let response = response as? HTTPURLResponse { } catch {
guard let data = data else { return }
// If we get a 201 CREATED, return the WFCollection as success; if not, return a WFError as failure.
if response.statusCode == 201 {
do {
let collection = try self.decoder.decode(ServerData<WFCollection>.self, from: data)
completion(.success(collection.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Retrieves a collection's metadata. /// Retrieves a collection's metadata.
@ -150,33 +123,19 @@ public class WFClient {
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in get(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
do {
let collection = try self.decoder.decode(ServerData<WFCollection>.self, from: data)
completion(.success(collection.data))
} catch {
completion(.failure(WFError.invalidData))
}
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
if let response = response as? HTTPURLResponse {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
do {
let collection = try self.decoder.decode(ServerData<WFCollection>.self, from: data)
completion(.success(collection.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
}
}
} }
dataTask.resume()
} }
/// Permanently deletes a collection. /// Permanently deletes a collection.
@ -202,48 +161,14 @@ public class WFClient {
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in delete(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(_):
// HACK: There's something that URLSession doesn't like about 204 NO CONTENT response that the API completion(.success(true))
// server is returning. If we get back a "protocol error", the operation probably succeeded, case .failure(let error):
// but URLSession is being pedantic/cranky and throwing an NSPOSIXErrorDomain error code 100. completion(.failure(error))
// Here, we check for that error, make sure the token was invalidated, and only then fire the
// success case in the completion block.
let nsError = error as NSError
if nsError.code == 100 && nsError.domain == NSPOSIXErrorDomain {
// Confirm that the operation succeeded by testing for a 404 on the same token.
self.deleteCollection(withAlias: alias) { result in
do {
_ = try result.get()
completion(.failure(error))
} catch WFError.notFound {
completion(.success(true))
} catch WFError.unauthorized {
completion(.success(true))
} catch {
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
if let response = response as? HTTPURLResponse {
// We got a response. If it's a 204 NO CONTENT, return true as success;
// if not, return a WFError as failure.
if response.statusCode != 204 {
guard let data = data else { return }
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
} else {
completion(.success(true))
}
} }
} }
dataTask.resume()
} }
// MARK: - Post-related methods // MARK: - Post-related methods
@ -282,40 +207,27 @@ public class WFClient {
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in get(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} // The response is formatted differently depending on if we're getting user posts or collection
// posts,so we need to determine what kind of structure we're decoding based on the
if let response = response as? HTTPURLResponse { // collectionAlias argument.
guard let data = data else { return } if collectionAlias != nil {
let post = try self.decoder.decode(NestedPostsJson.self, from: data)
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure. completion(.success(post.data))
if response.statusCode == 200 { } else {
do { let post = try self.decoder.decode(ServerData<[WFPost]>.self, from: data)
// The response is formatted differently depending on if we're getting user posts or collection completion(.success(post.data))
// posts,so we need to determine what kind of structure we're decoding based on the
// collectionAlias argument.
if collectionAlias != nil {
let post = try self.decoder.decode(NestedPostsJson.self, from: data)
completion(.success(post.data))
} else {
let post = try self.decoder.decode(ServerData<[WFPost]>.self, from: data)
completion(.success(post.data))
}
} catch {
completion(.failure(error))
} }
} else { } catch {
// We didn't get a 200 OK, so return a WFError.
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Moves a post to a collection. /// Moves a post to a collection.
@ -368,27 +280,14 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in post(with: request, expecting: 200) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(_):
completion(.success(true))
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
if let response = response as? HTTPURLResponse {
guard let data = data else { return }
// If we get a 200 OK, return true as success; if not, return a WFError as failure.
if response.statusCode == 200 {
completion(.success(true))
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
}
}
} }
dataTask.resume()
} }
/// Pins a post to a collection. /// Pins a post to a collection.
@ -442,27 +341,14 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in post(with: request, expecting: 200) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(_):
completion(.success(true))
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
if let response = response as? HTTPURLResponse {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
completion(.success(true))
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
}
}
} }
dataTask.resume()
} }
/// Unpins a post from a collection. /// Unpins a post from a collection.
@ -503,27 +389,14 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in post(with: request, expecting: 200) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(_):
completion(.success(true))
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
if let response = response as? HTTPURLResponse {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
completion(.success(true))
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
}
}
} }
dataTask.resume()
} }
/// Creates a new post. /// Creates a new post.
@ -581,33 +454,19 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in self.post(with: request, expecting: 201) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
if let response = response as? HTTPURLResponse { } catch {
guard let data = data else { return }
// If we get a 200 OK, return the WFPost as success; if not, return a WFError as failure.
if response.statusCode == 201 {
do {
let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Retrieves a post. /// Retrieves a post.
@ -632,33 +491,19 @@ public class WFClient {
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in get(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
if let response = response as? HTTPURLResponse { } catch {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
do {
let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Retrieves a post from a collection. /// Retrieves a post from a collection.
@ -689,33 +534,19 @@ public class WFClient {
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in get(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
if let response = response as? HTTPURLResponse { } catch {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
do {
let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Updates an existing post. /// Updates an existing post.
@ -762,33 +593,19 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in post(with: request, expecting: 200) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
if let response = response as? HTTPURLResponse { } catch {
guard let data = data else { return }
// If we get a 200 OK, return the WFPost as success; if not, return a WFError as failure.
if response.statusCode == 200 {
do {
let post = try self.decoder.decode(ServerData<WFPost>.self, from: data)
completion(.success(post.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Deletes an existing post. /// Deletes an existing post.
@ -817,51 +634,14 @@ public class WFClient {
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in delete(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(_):
// HACK: There's something that URLSession doesn't like about 204 NO CONTENT response that the API completion(.success(true))
// server is returning. If we get back a "protocol error", the operation probably succeeded, case .failure(let error):
// but URLSession is being pedantic/cranky and throwing an NSPOSIXErrorDomain error code 100. completion(.failure(error))
// Here, we check for that error, make sure the token was invalidated, and only then fire the
// success case in the completion block.
let nsError = error as NSError
if nsError.code == 100 && nsError.domain == NSPOSIXErrorDomain {
// Confirm that the operation succeeded by testing for a 404 on the same token.
self.deletePost(postId: postId) { result in
do {
_ = try result.get()
completion(.failure(error))
} catch WFError.notFound {
completion(.success(true))
} catch WFError.unauthorized {
completion(.success(true))
} catch WFError.internalServerError {
// If you try to delete a non-existent post, the API returns a 500 Internal Server Error.
completion(.success(true))
} catch {
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
if let response = response as? HTTPURLResponse {
// We got a response. If it's a 204 NO CONTENT, return true as success;
// if not, return a WFError as failure.
if response.statusCode != 204 {
guard let data = data else { return }
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
} else {
completion(.success(true))
}
} }
} }
dataTask.resume()
} }
/* Placeholder method stub: API design for this feature is not yet finalized. /* Placeholder method stub: API design for this feature is not yet finalized.
@ -903,37 +683,20 @@ public class WFClient {
completion(.failure(error)) completion(.failure(error))
} }
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in post(with: request, expecting: 200) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let user = try self.decoder.decode(WFUser.self, from: data)
self.user = user
if let response = response as? HTTPURLResponse { completion(.success(user))
guard let data = data else { return } } catch {
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
do {
let user = try self.decoder.decode(WFUser.self, from: data)
self.user = user
completion(.success(user))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError
guard let error = self.translateWFError(fromServerResponse: data) else {
// We couldn't generate a WFError from the server response data, so return an unknown error.
completion(.failure(WFError.unknownError))
return
}
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
/// Invalidates the user's access token. /// Invalidates the user's access token.
@ -953,51 +716,15 @@ public class WFClient {
request.addValue(tokenToDelete, forHTTPHeaderField: "Authorization") request.addValue(tokenToDelete, forHTTPHeaderField: "Authorization")
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in delete(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(_):
// HACK: There's something that URLSession doesn't like about 204 NO CONTENT response that the API self.user = nil
// server is returning. If we get back a "protocol error", the operation probably succeeded, completion(.success(true))
// but URLSession is being pedantic/cranky and throwing an NSPOSIXErrorDomain error code 100. case .failure(let error):
// Here, we check for that error, make sure the token was invalidated, and only then fire the completion(.failure(error))
// success case in the completion block.
let nsError = error as NSError
if nsError.code == 100 && nsError.domain == NSPOSIXErrorDomain {
// Confirm that the operation succeeded by testing for a 404 on the same token.
self.logout(token: tokenToDelete) { result in
do {
_ = try result.get()
completion(.failure(error))
} catch WFError.notFound {
self.user = nil
completion(.success(true))
} catch WFError.unauthorized {
self.user = nil
completion(.success(true))
} catch {
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
if let response = response as? HTTPURLResponse {
// We got a response. If it's a 204 NO CONTENT, return true as success;
// if not, return a WFError as failure.
if response.statusCode != 204 {
guard let data = data else { return }
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
} else {
self.user = nil
completion(.success(true))
}
} }
} }
dataTask.resume()
} }
/// Retrieves a user's basic data. /// Retrieves a user's basic data.
@ -1016,27 +743,14 @@ public class WFClient {
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in get(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.success(data))
case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
if let response = response as? HTTPURLResponse {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
completion(.success(data))
} else {
// We didn't get a 200 OK, so return a WFError.
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error))
}
}
} }
dataTask.resume()
} }
/// Retrieves a user's collections. /// Retrieves a user's collections.
@ -1055,32 +769,19 @@ public class WFClient {
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization") request.addValue(tokenToVerify, forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in get(with: request) { result in
// Something went wrong; return the error message. switch result {
if let error = error { case .success(let data):
completion(.failure(error)) do {
} let collection = try self.decoder.decode(ServerData<[WFCollection]>.self, from: data)
completion(.success(collection.data))
if let response = response as? HTTPURLResponse { } catch {
guard let data = data else { return }
// If we get a 200 OK, return the WFUser as success; if not, return a WFError as failure.
if response.statusCode == 200 {
do {
let collection = try self.decoder.decode(ServerData<[WFCollection]>.self, from: data)
completion(.success(collection.data))
} catch {
completion(.failure(error))
}
} else {
// We didn't get a 200 OK, so return a WFError.
guard let error = self.translateWFError(fromServerResponse: data) else { return }
completion(.failure(error)) completion(.failure(error))
} }
case .failure(let error):
completion(.failure(error))
} }
} }
dataTask.resume()
} }
} }
@ -1096,3 +797,16 @@ private extension WFClient {
} }
} }
} }
// MARK: - Protocol conformance
extension URLSession: URLSessionProtocol {
public func dataTask(
with request: URLRequest,
completionHandler: @escaping DataTaskResult
) -> URLSessionDataTaskProtocol {
return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTask
}
}
extension URLSessionDataTask: URLSessionDataTaskProtocol {}

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
public enum WFError: Int, Error { public enum WFError: Int, Error {
// Errors returned by the server
case badRequest = 400 case badRequest = 400
case unauthorized = 401 case unauthorized = 401
case forbidden = 403 case forbidden = 403
@ -12,7 +13,12 @@ public enum WFError: Int, Error {
case internalServerError = 500 case internalServerError = 500
case badGateway = 502 case badGateway = 502
case serviceUnavailable = 503 case serviceUnavailable = 503
// Other errors
case unknownError = -1 case unknownError = -1
case couldNotComplete = -2
case invalidResponse = -3
case invalidData = -4
} }
struct ErrorMessage: Codable { struct ErrorMessage: Codable {

View File

@ -1,7 +0,0 @@
# Types
- [WFClient](/WFClient)
- [WFCollection](/WFCollection)
- [WFError](/WFError)
- [WFPost](/WFPost)
- [WFUser](/WFUser)

View File

@ -1,329 +0,0 @@
# WFClient
``` swift
public class WFClient
```
## Initializers
### `init(for:)`
Initializes the WriteFreely client.
``` swift
public init(for instanceURL: URL)
```
Required for connecting to the API endpoints of a WriteFreely instance.
#### Parameters
- instanceURL: - instanceURL: The URL for the WriteFreely instance to which we're connecting, including the protocol.
## Properties
### `decoder`
``` swift
let decoder
```
### `requestURL`
``` swift
var requestURL: URL
```
### `user`
``` swift
var user: WFUser?
```
## Methods
### `createCollection(token:withTitle:alias:completion:)`
Creates a new collection.
``` swift
public func createCollection(token: String? = nil, withTitle title: String, alias: String? = nil, completion: @escaping (Result<WFCollection, Error>) -> Void)
```
If only a `title` is given, the server will generate and return an alias; in this case, clients should store
the returned `alias` for future operations.
#### Parameters
- token: - token: The access token for the user creating the collection.
- title: - title: The title of the new collection.
- alias: - alias: The alias of the collection.
- completion: - completion: A handler for the returned `WFCollection` on success, or `Error` on failure.
### `getCollection(token:withAlias:completion:)`
Retrieves a collection's metadata.
``` swift
public func getCollection(token: String? = nil, withAlias alias: String, completion: @escaping (Result<WFCollection, Error>) -> Void)
```
Collections can be retrieved without authentication. However, authentication is required for retrieving a
private collection or one with scheduled posts.
#### Parameters
- token: - token: The access token for the user retrieving the collection.
- alias: - alias: The alias for the collection to be retrieved.
- completion: - completion: A handler for the returned `WFCollection` on success, or `Error` on failure.
### `deleteCollection(token:withAlias:completion:)`
Permanently deletes a collection.
``` swift
public func deleteCollection(token: String? = nil, withAlias alias: String, completion: @escaping (Result<Bool, Error>) -> Void)
```
Any posts in the collection are not deleted; rather, they are made anonymous.
#### Parameters
- token: - token: The access token for the user deleting the collection.
- alias: - alias: The alias for the collection to be deleted.
- completion: - completion: A hander for the returned `Bool` on success, or `Error` on failure.
### `getPosts(token:in:completion:)`
Retrieves an array of posts.
``` swift
public func getPosts(token: String? = nil, in collectionAlias: String? = nil, completion: @escaping (Result<[WFPost], Error>) -> Void)
```
If the `collectionAlias` argument is provided, an array of all posts in that collection is retrieved; if
omitted, an array of all posts created by the user whose access token is provided is retrieved.
Collection posts can be retrieved without authentication; however, authentication is required for retrieving a
private collection or one with scheduled posts.
#### Parameters
- token: - token: The access token for the user retrieving the posts.
- collectionAlias: - collectionAlias: The alias for the collection whose posts are to be retrieved.
- completion: - completion: A handler for the returned `[WFPost]` on success, or `Error` on failure.
### `movePost(token:postId:with:to:completion:)`
Moves a post to a collection.
``` swift
public func movePost(token: String? = nil, postId: String, with modifyToken: String? = nil, to collectionAlias: String, completion: @escaping (Result<Bool, Error>) -> Void)
```
> Attention: - The closure should return a result type of \`\<\[WFPost\], Error\>\`.
> - The modifyToken for the post is currently ignored.
>
#### Parameters
- token: - token: The access token for the user moving the post to a collection.
- postId: - postId: The ID of the post to add to the collection.
- modifyToken: - modifyToken: The post's modify token; required if the post doesn't belong to the requesting user.
- collectionAlias: - collectionAlias: The alias of the collection to which the post should be added.
- completion: - completion: A handler for the returned `Bool` on success, or `Error` on failure.
### `pinPost(token:postId:at:in:completion:)`
Pins a post to a collection.
``` swift
public func pinPost(token: String? = nil, postId: String, at position: Int? = nil, in collectionAlias: String, completion: @escaping (Result<Bool, Error>) -> Void)
```
Pinning a post to a collection adds it as a navigation item in the collection/blog home page header, rather
than on the blog itself. While the API endpoint can take an array of posts, this function only accepts a single
post.
#### Parameters
- token: - token: The access token of the user pinning the post to the collection.
- postId: - postId: The ID of the post to be pinned.
- position: - position: The numeric position in which to pin the post; if `nil`, will pin at the end of the list.
- collectionAlias: - collectionAlias: The alias of the collection to which the post should be pinned.
- completion: - completion: A handler for the `Bool` returned on success, or `Error` on failure.
### `unpinPost(token:postId:from:completion:)`
Unpins a post from a collection.
``` swift
public func unpinPost(token: String? = nil, postId: String, from collectionAlias: String, completion: @escaping (Result<Bool, Error>) -> Void)
```
Removes the post from a navigation item and puts it back on the blog itself. While the API endpoint can take an
array of posts, this function only accepts a single post.
#### Parameters
- token: - token: The access token of the user un-pinning the post from the collection.
- postId: - postId: The ID of the post to be un-pinned.
- collectionAlias: - collectionAlias: The alias of the collection to which the post should be un-pinned.
- completion: - completion: A handler for the `Bool` returned on success, or `Error` on failure.
### `createPost(token:post:in:completion:)`
Creates a new post.
``` swift
public func createPost(token: String? = nil, post: WFPost, in collectionAlias: String? = nil, completion: @escaping (Result<WFPost, Error>) -> Void)
```
Creates a new post. If a `collectionAlias` is provided, the post is published to that collection; otherwise, it
is posted to the user's Drafts.
#### Parameters
- token: - token: The access token of the user creating the post.
- post: - post: The `WFPost` object to be published.
- collectionAlias: - collectionAlias: The collection to which the post should be published.
- completion: - completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `getPost(token:byId:completion:)`
Retrieves a post.
``` swift
public func getPost(token: String? = nil, byId postId: String, completion: @escaping (Result<WFPost, Error>) -> Void)
```
The `WFPost` object returned may include additional data, including page views and extracted tags.
#### Parameters
- token: - token: The access token of the user retrieving the post.
- postId: - postId: The ID of the post to be retrieved.
- completion: - completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `getPost(token:bySlug:from:completion:)`
Retrieves a post from a collection.
``` swift
public func getPost(token: String? = nil, bySlug slug: String, from collectionAlias: String, completion: @escaping (Result<WFPost, Error>) -> Void)
```
Collection posts can be retrieved without authentication. However, authentication is required for retrieving a
post from a private collection.
The `WFPost` object returned may include additional data, including page views and extracted tags.
#### Parameters
- token: - token: The access token of the user retrieving the post.
- slug: - slug: The slug of the post to be retrieved.
- collectionAlias: - collectionAlias: The alias of the collection from which the post should be retrieved.
- completion: - completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `updatePost(token:postId:updatedPost:with:completion:)`
Updates an existing post.
``` swift
public func updatePost(token: String? = nil, postId: String, updatedPost: WFPost, with modifyToken: String? = nil, completion: @escaping (Result<WFPost, Error>) -> Void)
```
Note that if the `updatedPost` object is provided without a title, the original post's title will be removed.
> Attention: - The modifyToken for the post is currently ignored.
>
#### Parameters
- token: - token: The access token for the user updating the post.
- postId: - postId: The ID of the post to be updated.
- updatedPost: - updatedPost: The `WFPost` object with which to update the existing post.
- modifyToken: - modifyToken: The post's modify token; required if the post doesn't belong to the requesting user.
- completion: - completion: A handler for the `WFPost` object returned on success, or `Error` on failure.
### `deletePost(token:postId:with:completion:)`
Deletes an existing post.
``` swift
public func deletePost(token: String? = nil, postId: String, with modifyToken: String? = nil, completion: @escaping (Result<Bool, Error>) -> Void)
```
> Attention: - The modifyToken for the post is currently ignored.
>
#### Parameters
- token: - token: The access token for the user deleting the post.
- postId: - postId: The ID of the post to be deleted.
- modifyToken: - modifyToken: The post's modify token; required if the post doesn't belong to the requesting user.
- completion: - completion: A handler for the `Bool` object returned on success, or `Error` on failure.
### `login(username:password:completion:)`
Logs the user in to their account on the WriteFreely instance.
``` swift
public func login(username: String, password: String, completion: @escaping (Result<WFUser, Error>) -> Void)
```
On successful login, the `WFClient`'s `user` property is set to the returned `WFUser` object; this allows
authenticated requests to be made without having to provide an access token.
It is otherwise not necessary to login the user if their access token is provided to the calling function.
#### Parameters
- username: - username: The user's username.
- password: - password: The user's password.
- completion: - completion: A handler for the `WFUser` object returned on success, or `Error` on failure.
### `logout(token:completion:)`
Invalidates the user's access token.
``` swift
public func logout(token: String? = nil, completion: @escaping (Result<Bool, Error>) -> Void)
```
#### Parameters
- token: - token: The token to invalidate.
- completion: - completion: A handler for the `Bool` object returned on success, or `Error` on failure.
### `getUserData(token:completion:)`
Retrieves a user's basic data.
``` swift
public func getUserData(token: String? = nil, completion: @escaping (Result<Data, Error>) -> Void)
```
#### Parameters
- token: - token: The access token for the user to fetch.
- completion: - completion: A handler for the `Data` object returned on success, or `Error` on failure.
### `getUserCollections(token:completion:)`
Retrieves a user's collections.
``` swift
public func getUserCollections(token: String? = nil, completion: @escaping (Result<[WFCollection], Error>) -> Void)
```
#### Parameters
- token: - token: The access token for the user whose collections are to be retrieved.
- completion: - completion: A handler for the `[WFCollection]` object returned on success, or `Error` on failure.
### `translateWFError(fromServerResponse:)`
``` swift
func translateWFError(fromServerResponse response: Data) -> WFError?
```

View File

@ -1,77 +0,0 @@
# WFError
``` swift
public enum WFError
```
## Inheritance
`Error`, `Int`
## Enumeration Cases
### `badRequest`
``` swift
case badRequest
```
### `unauthorized`
``` swift
case unauthorized
```
### `forbidden`
``` swift
case forbidden
```
### `notFound`
``` swift
case notFound
```
### `methodNotAllowed`
``` swift
case methodNotAllowed
```
### `gone`
``` swift
case gone
```
### `preconditionFailed`
``` swift
case preconditionFailed
```
### `tooManyRequests`
``` swift
case tooManyRequests
```
### `internalServerError`
``` swift
case internalServerError
```
### `badGateway`
``` swift
case badGateway
```
### `serviceUnavailable`
``` swift
case serviceUnavailable
```

View File

@ -1 +0,0 @@
Generated at 2020-08-31T10:27:33-0400 using [swift-doc](https://github.com/SwiftDocOrg/swift-doc) 1.0.0-beta.3.

View File

@ -1,10 +0,0 @@
<details>
<summary>Types</summary>
- [WFClient](/WFClient)
- [WFCollection](/WFCollection)
- [WFError](/WFError)
- [WFPost](/WFPost)
- [WFUser](/WFUser)
</details>