Skip to content

Commit

Permalink
0.26.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Mar 10, 2024
1 parent cc8abee commit a24d93f
Show file tree
Hide file tree
Showing 14 changed files with 365 additions and 131 deletions.
6 changes: 3 additions & 3 deletions Example/Sources/PetStore/CustomDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import Foundation
import SwiftNetworking

struct PetStoreDecoder: DataDecoder {
struct PetStoreDecoder: DataDecoder {

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode( PetStoreResponse<T>.self, from: data)
let response = try decoder.decode(PetStoreResponse<T>.self, from: data)
guard let result = response.response, response.success else {
throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "Server error"))
}
return result
}
}

struct PetStoreResponse<T: Decodable>: Decodable {
struct PetStoreResponse<T: Decodable>: Decodable {

var success: Bool
var error: String?
Expand Down
4 changes: 2 additions & 2 deletions Example/Sources/PetStore/ExampleOfCalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ func exampleOfAPICalls() async throws {
_ = try await api().user("name").delete()
}

func api(fileID: String = #fileID, line: UInt = #line) -> PetStore {
PetStore(baseURL: .production, fileID: fileID, line: line)
func api(fileID: String = #fileID, line: UInt = #line) -> PetStore {
PetStore(baseURL: .production, fileID: fileID, line: line)
}
10 changes: 5 additions & 5 deletions Example/Sources/PetStore/PetStore.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Foundation
import SwiftNetworking

public struct PetStore {
public struct PetStore {

// MARK: - BaseURL

var client: NetworkClient

public init(baseURL: BaseURL, fileID: String, line: UInt) {
client = NetworkClient(baseURL: baseURL.url)
client = NetworkClient(baseURL: baseURL.url)
.fileIDLine(fileID: fileID, line: line)
.bodyDecoder(PetStoreDecoder())
.bearerAuth(
Expand All @@ -28,7 +28,7 @@ public struct PetStore {

// MARK: - "pet" path

public extension PetStore {
public extension PetStore {

var pet: Pet {
Pet(client: client("pet"))
Expand Down Expand Up @@ -89,7 +89,7 @@ public extension PetStore {

// MARK: - "store" path

public extension PetStore {
public extension PetStore {

var store: Store {
Store(client: client("store").auth(enabled: false))
Expand Down Expand Up @@ -128,7 +128,7 @@ public extension PetStore {

// MARK: "user" path

public extension PetStore {
public extension PetStore {

var user: User {
User(client: client("user").auth(enabled: false))
Expand Down
24 changes: 12 additions & 12 deletions Example/Sources/PetStore/PetStoreBaseURL.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Foundation
import SwiftNetworking

extension PetStore {
// MARK: - BaseURL
public enum BaseURL: String {
case production = "https://petstore.com"
case staging = "https://staging.petstore.com"
case test = "http://localhost:8080"
public var url: URL { URL(string: rawValue)! }
}
public extension PetStore {

// MARK: - BaseURL

enum BaseURL: String {

case production = "https://petstore.com"
case staging = "https://staging.petstore.com"
case test = "http://localhost:8080"

public var url: URL { URL(string: rawValue)! }
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ The `.decodable` serializer uses the `.bodyDecoder` configuration, which can be

### Encoding and Decoding
There are several built-in configurations for encoding and decoding:
- `.bodyEncoder` for encoding a request body. Built-in encoders include `.json` and `.formURL`.
- `.bodyEncoder` for encoding a request body. Built-in encoders include `.json`, `.formURL` and `.multipartFormData`.
- `.bodyDecoder` for decoding a request body. The built-in decoder is `.json`.
- `.queryEncoder` for encoding a query. The built-in encoder is `.query`.
- `.errorDecoder` for decoding an error response. The built-in decoder is `.decodable(type)`.
Expand Down Expand Up @@ -208,7 +208,7 @@ import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-networking.git", from: "0.25.0")
.package(url: "https://github.com/dankinsoid/swift-networking.git", from: "0.26.0")
],
targets: [
.target(
Expand Down
20 changes: 10 additions & 10 deletions Sources/SwiftNetworking/NetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ public struct NetworkClient {
_createRequest = createRequest
}

/// Initializes a new network client with a closure that creates a URLRequest.
/// - Parameter baseURL: A closure that takes `Configs` and returns a `URL`.
public init(
baseURL: @escaping (Configs) throws -> URL
) {
self.init {
try URLRequest(url: baseURL($0))
}
}
/// Initializes a new network client with a closure that creates a URLRequest.
/// - Parameter baseURL: A closure that takes `Configs` and returns a `URL`.
public init(
baseURL: @escaping (Configs) throws -> URL
) {
self.init {
try URLRequest(url: baseURL($0))
}
}

/// Initializes a new network client with a base URL for requests.
/// - Parameter baseURL: The base URL to be used for creating requests.
public init(
Expand Down
8 changes: 6 additions & 2 deletions Sources/SwiftNetworking/Types/ContentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,12 @@ public extension ContentType {

/// Creates a content type for `multipart` with a specific subtype. You can use a string literal as well.
/// Example: `multipart/form-data`
static func multipart(_ subtype: Multipart) -> ContentType {
ContentType("multipart", subtype.rawValue)
static func multipart(_ subtype: Multipart, boundary: String? = nil) -> ContentType {
var result = ContentType("multipart", subtype.rawValue)
if let boundary {
result.parameters["boundary"] = boundary
}
return result
}

/// `*/*`
Expand Down
10 changes: 8 additions & 2 deletions Sources/SwiftNetworking/Types/HTTPHeaders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,14 @@ public extension HTTPHeader {
/// - Parameter value: The `Content-Disposition` value.
///
/// - Returns: The header.
static func contentDisposition(_ value: String) -> HTTPHeader {
HTTPHeader(.contentDisposition, value)
static func contentDisposition(_ type: String, name: String, filename: String? = nil) -> HTTPHeader {
let nameEncoded = name.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? name
var value = "form-data; name=\"\(nameEncoded)\""
if let filename {
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? filename
value += "; filename=\"\(filenameEncoded)\""
}
return HTTPHeader(.contentDisposition, value)
}

/// Returns a `Content-Encoding` header.
Expand Down
40 changes: 20 additions & 20 deletions Sources/SwiftNetworking/Types/LoggingComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,28 +103,28 @@ public extension LoggingComponents {
return message
}

func responseMessage(
for response: HTTPURLResponse,
uuid: UUID,
data: Data?,
duration: TimeInterval,
error: Error? = nil
) -> String {
responseMessage(
uuid: uuid,
statusCode: response.httpStatusCode,
data: data,
headers: response.headers,
duration: duration,
error: error
)
}
func responseMessage(
for response: HTTPURLResponse,
uuid: UUID,
data: Data?,
duration: TimeInterval,
error: Error? = nil
) -> String {
responseMessage(
uuid: uuid,
statusCode: response.httpStatusCode,
data: data,
headers: response.headers,
duration: duration,
error: error
)
}

func responseMessage(
uuid: UUID,
uuid: UUID,
statusCode: HTTPStatusCode? = nil,
data: Data?,
headers: HTTPHeaders = [],
headers: HTTPHeaders = [],
duration: TimeInterval? = nil,
error: Error? = nil
) -> String {
Expand All @@ -139,8 +139,8 @@ public extension LoggingComponents {
message.append("🛑")
}
var isMultiline = false
if let statusCode, contains(.statusCode) {
message += " \(statusCode.rawValue) \(statusCode.name)"
if let statusCode, contains(.statusCode) {
message += " \(statusCode.rawValue) \(statusCode.name)"
}
var inBrackets: [String] = []
if let duration, contains(.duration) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Foundation

/// RFC 7528 multipart/form-data
/// 4. Definition of multipart/form-data
///
/// The media type multipart/form-data follows the model of multipart
/// MIME data streams as specified in Section 5.1 of [RFC2046]; changes
/// are noted in this document.
///
/// A multipart/form-data body contains a series of parts separated by a
/// boundary.
public struct MultipartFormData: Hashable {

public var parts: [Part]

/// 4.1. "Boundary" Parameter of multipart/form-data
///
/// As with other multipart types, the parts are delimited with a
/// boundary delimiter, constructed using CRLF, "--", and the value of
/// the "boundary" parameter. The boundary is supplied as a "boundary"
/// parameter to the multipart/form-data type. As noted in Section 5.1
/// of [RFC2046], the boundary delimiter MUST NOT appear inside any of
/// the encapsulated parts, and it is often necessary to enclose the
/// "boundary" parameter values in quotes in the Content-Type header
/// field.
public var boundary: String

public init(parts: [Part], boundary: String) {
self.parts = parts
self.boundary = boundary
}

public var data: Data {
var data = Data()
let boundaryData = Data(boundary.utf8)
for part in parts {
data.append(DASH)
data.append(boundaryData)
data.append(CRLF)
part.write(to: &data)
}

data.append(DASH)
data.append(boundaryData)
data.append(DASH)
data.append(CRLF)
return data
}
}

public extension MultipartFormData {

struct Part: Hashable {

/// Each part MUST contain a Content-Disposition header field [RFC2183]
/// where the disposition type is "form-data". The Content-Disposition
/// header field MUST also contain an additional parameter of "name"; the
/// value of the "name" parameter is the original field name from the
/// form (possibly encoded; see Section 5.1). For example, a part might
/// contain a header field such as the following, with the body of the
/// part containing the form data of the "user" field:
///
/// Content-Disposition: form-data; name="user"
///
public let name: String

/// For form data that represents the content of a file, a name for the
/// file SHOULD be supplied as well, by using a "filename" parameter of
/// the Content-Disposition header field. The file name isn't mandatory
/// for cases where the file name isn't available or is meaningless or
/// private; this might result, for example, when selection or drag-and-
/// drop is used or when the form data content is streamed directly from
/// a device.
public let filename: String?
public let mimeType: ContentType?
public let content: Data

/// RFC 2046 Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types
/// 5.1.1. Common Syntax
///
/// ...
///
/// This Content-Type value indicates that the content consists of one or
/// more parts, each with a structure that is syntactically identical to
/// an RFC 822 message, except that the header area is allowed to be
/// completely empty, and that the parts are each preceded by the line
///
/// --gc0pJq0M:08jU534c0p
///
/// The boundary delimiter MUST occur at the beginning of a line, i.e.,
/// following a CRLF, and the initial CRLF is considered to be attached
/// to the boundary delimiter line rather than part of the preceding
/// part. The boundary may be followed by zero or more characters of
/// linear whitespace. It is then terminated by either another CRLF and
/// the header fields for the next part, or by two CRLFs, in which case
/// there are no header fields for the next part. If no Content-Type
/// field is present it is assumed to be "message/rfc822" in a
/// "multipart/digest" and "text/plain" otherwise.
public func write(to data: inout Data) {
let header = HTTPHeader.contentDisposition("form-data", name: name, filename: filename)
let contentDispositionData = Data(header.description.utf8)

data.append(contentDispositionData)
data.append(CRLF)
if let mimeType {
let contentTypeHeader = HTTPHeader.contentType(mimeType)
let contentTypeData = Data(contentTypeHeader.description.utf8)
data.append(contentTypeData)
data.append(CRLF)
}
data.append(CRLF)
data.append(content)
data.append(CRLF)
}

public init(
name: String,
filename: String? = nil,
mimeType: ContentType?,
data: Data
) {
self.name = name
self.filename = filename
self.mimeType = mimeType
content = data
}
}
}

private let CRLF: Data = "\r\n".data(using: .ascii)!
private let DASH = "--".data(using: .utf8)!
Loading

0 comments on commit a24d93f

Please sign in to comment.