Skip to content

[Multipart] Validation sequence #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions Sources/OpenAPIRuntime/Base/ContentDisposition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A parsed representation of the `content-disposition` header described by RFC 6266 containing only
/// the features relevant to OpenAPI multipart bodies.
struct ContentDisposition: Hashable {

/// A `disposition-type` parameter value.
enum DispositionType: Hashable {

/// A form data value.
case formData

/// Any other value.
case other(String)

/// Creates a new disposition type value.
/// - Parameter rawValue: A string representation of the value.
init(rawValue: String) {
switch rawValue.lowercased() {
case "form-data": self = .formData
default: self = .other(rawValue)
}
}

/// A string representation of the value.
var rawValue: String {
switch self {
case .formData: return "form-data"
case .other(let string): return string
}
}
}

/// The disposition type value.
var dispositionType: DispositionType

/// A content disposition parameter name.
enum ParameterName: Hashable {

/// The name parameter.
case name

/// The filename parameter.
case filename

/// Any other parameter.
case other(String)

/// Creates a new parameter name.
/// - Parameter rawValue: A string representation of the name.
init(rawValue: String) {
switch rawValue.lowercased() {
case "name": self = .name
case "filename": self = .filename
default: self = .other(rawValue)
}
}

/// A string representation of the name.
var rawValue: String {
switch self {
case .name: return "name"
case .filename: return "filename"
case .other(let string): return string
}
}
}

/// The parameters of the content disposition value.
var parameters: [ParameterName: String] = [:]

/// The name parameter value.
var name: String? {
get { parameters[.name] }
set { parameters[.name] = newValue }
}

/// The filename parameter value.
var filename: String? {
get { parameters[.filename] }
set { parameters[.filename] = newValue }
}
}

extension ContentDisposition: RawRepresentable {

/// Creates a new instance with the specified raw value.
///
/// https://datatracker.ietf.org/doc/html/rfc6266#section-4.1
/// - Parameter rawValue: The raw value to use for the new instance.
init?(rawValue: String) {
var components = rawValue.split(separator: ";").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard !components.isEmpty else { return nil }
self.dispositionType = DispositionType(rawValue: components.removeFirst())
let parameterTuples: [(ParameterName, String)] = components.compactMap { component in
let parameterComponents = component.split(separator: "=", maxSplits: 1)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard parameterComponents.count == 2 else { return nil }
let valueWithoutQuotes = parameterComponents[1].trimmingCharacters(in: ["\""])
return (.init(rawValue: parameterComponents[0]), valueWithoutQuotes)
}
self.parameters = Dictionary(parameterTuples, uniquingKeysWith: { a, b in a })
}

/// The corresponding value of the raw type.
var rawValue: String {
var string = ""
string.append(dispositionType.rawValue)
if !parameters.isEmpty {
for (key, value) in parameters.sorted(by: { $0.key.rawValue < $1.key.rawValue }) {
string.append("; \(key.rawValue)=\"\(value)\"")
}
}
return string
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import HTTPTypes

// MARK: - Extensions

extension MultipartRawPart {

/// Creates a new raw part by injecting the provided name and filename into
/// the `content-disposition` header field.
/// - Parameters:
/// - name: The name of the part.
/// - filename: The file name of the part.
/// - headerFields: The header fields of the part.
/// - body: The body stream of the part.
public init(name: String?, filename: String? = nil, headerFields: HTTPFields, body: HTTPBody) {
var parameters: [ContentDisposition.ParameterName: String] = [:]
if let name { parameters[.name] = name }
if let filename { parameters[.filename] = filename }
let contentDisposition = ContentDisposition(dispositionType: .formData, parameters: parameters)
var headerFields = headerFields
headerFields[.contentDisposition] = contentDisposition.rawValue
self.init(headerFields: headerFields, body: body)
}

/// Returns the parameter value for the provided name.
/// - Parameter name: The parameter name.
/// - Returns: The parameter value. Nil if not found in the content disposition header field.
private func getParameter(_ name: ContentDisposition.ParameterName) -> String? {
guard let contentDispositionString = headerFields[.contentDisposition],
let contentDisposition = ContentDisposition(rawValue: contentDispositionString)
else { return nil }
return contentDisposition.parameters[name]
}

/// Sets the parameter name to the provided value.
/// - Parameters:
/// - name: The parameter name.
/// - value: The value of the parameter.
private mutating func setParameter(_ name: ContentDisposition.ParameterName, _ value: String?) {
guard let contentDispositionString = headerFields[.contentDisposition],
var contentDisposition = ContentDisposition(rawValue: contentDispositionString)
else {
if let value {
headerFields[.contentDisposition] =
ContentDisposition(dispositionType: .formData, parameters: [name: value]).rawValue
}
return
}
contentDisposition.parameters[name] = value
headerFields[.contentDisposition] = contentDisposition.rawValue
}

/// The name of the part stored in the `content-disposition` header field.
public var name: String? {
get { getParameter(.name) }
set { setParameter(.name, newValue) }
}

/// The file name of the part stored in the `content-disposition` header field.
public var filename: String? {
get { getParameter(.filename) }
set { setParameter(.filename, newValue) }
}
}
Loading