From 2d5b88fdaca0cc428642b4b23adfa2c3a4dcc941 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 26 Feb 2026 10:03:10 +0100 Subject: [PATCH 1/3] remove trimmingcharacters --- .../Base/ContentDisposition.swift | 11 ++-- .../Conversion/Converter+Server.swift | 2 +- .../Conversion/FoundationExtensions.swift | 18 +++++-- .../Test_FoundationExtensions.swift | 50 +++++++++++++++++++ 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift diff --git a/Sources/OpenAPIRuntime/Base/ContentDisposition.swift b/Sources/OpenAPIRuntime/Base/ContentDisposition.swift index eaac4fd..bab6283 100644 --- a/Sources/OpenAPIRuntime/Base/ContentDisposition.swift +++ b/Sources/OpenAPIRuntime/Base/ContentDisposition.swift @@ -12,8 +12,11 @@ // //===----------------------------------------------------------------------===// -// Full Foundation needed for String.trimmingCharacters +#if canImport(FoundationEssentials) +import FoundationModels +#else import Foundation +#endif /// A parsed representation of the `content-disposition` header described by RFC 6266 containing only /// the features relevant to OpenAPI multipart bodies. @@ -104,15 +107,15 @@ extension ContentDisposition: RawRepresentable { /// 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) } + var components = rawValue.split(separator: ";").map { $0.trimmingLeadingAndTrailingSpaces } guard !components.isEmpty else { return nil } self.dispositionType = DispositionType(rawValue: components.removeFirst()) let parameterTuples: [(ParameterName, String)] = components.compactMap { (component: String) -> (ParameterName, String)? in let parameterComponents = component.split(separator: "=", maxSplits: 1) - .map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } + .map { $0.trimmingLeadingAndTrailingSpaces } guard parameterComponents.count == 2 else { return nil } - let valueWithoutQuotes = parameterComponents[1].trimmingCharacters(in: ["\""]) + let valueWithoutQuotes = parameterComponents[1].trimming(while: { $0 == "\"" }) return (.init(rawValue: parameterComponents[0]), valueWithoutQuotes) } self.parameters = Dictionary(parameterTuples, uniquingKeysWith: { a, b in a }) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 03746e3..7394568 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -55,7 +55,7 @@ extension Converter { let acceptValues = acceptHeader.split(separator: ",") .map { value in // Drop everything after the optional semicolon (q, extensions, ...) - value.split(separator: ";")[0].trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + value.split(separator: ";")[0].trimmingLeadingAndTrailingSpaces.lowercased() } if acceptValues.isEmpty { return } guard let parsedSubstring = OpenAPIMIMEType(substring) else { diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index f58c24f..50dba1f 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -12,11 +12,23 @@ // //===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif -extension String { - +extension StringProtocol { /// Returns the string with leading and trailing whitespace (such as spaces /// and newlines) removed. - var trimmingLeadingAndTrailingSpaces: Self { trimmingCharacters(in: .whitespacesAndNewlines) } + var trimmingLeadingAndTrailingSpaces: String { self.trimming { $0.isWhitespace } } + + /// Returns a new string by removing leading and trailing characters + /// that satisfy the given predicate. + func trimming(while predicate: (Character) -> Bool) -> String { + guard let start = self.firstIndex(where: { !predicate($0) }) else { return "" } + let end = self.lastIndex(where: { !predicate($0) })! + + return String(self[start...end]) + } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift new file mode 100644 index 0000000..5e35498 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2026 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 XCTest +import Foundation +@testable import OpenAPIRuntime + +final class Test_FoundationExtensions: Test_Runtime { + + func testTrimmingMatchesFoundationBehavior() { + let testCases = [ + " Hello World ", // Standard spaces + "\n\tHello World\r\n", // Newlines and tabs + "NoTrimmingNeeded", // No whitespace + " ", // Only spaces + "", // Empty string + " Hello\nWorld ", // Internal whitespace (should stay) + "\u{00A0}Unicode\u{00A0}", // Non-breaking space + "\u{3000}Ideographic\u{3000}", // Japanese/Chinese full-width space + ] + + for input in testCases { + let foundationResult = input.trimmingCharacters(in: .whitespacesAndNewlines) + let swiftNativeResult = input.trimmingLeadingAndTrailingSpaces + + XCTAssertEqual(swiftNativeResult, foundationResult, "Failed for input: \(input)") + } + } + + func testGenericTrimming() { + // Testing the "Generic" power of the function + let numericString = "0001234500" + let result = numericString.trimming(while: { $0 == "0" }) + XCTAssertEqual(result, "12345") + + let punctuationString = "...Hello!.." + let puncResult = punctuationString.trimming(while: { $0.isPunctuation }) + XCTAssertEqual(puncResult, "Hello") + } +} From e911af0cfa2ff7f85dcd63ca44b322903e296d12 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 26 Feb 2026 10:12:43 +0100 Subject: [PATCH 2/3] remove comment --- .../Conversion/Test_FoundationExtensions.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift index 5e35498..1a21a27 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift @@ -38,7 +38,6 @@ final class Test_FoundationExtensions: Test_Runtime { } func testGenericTrimming() { - // Testing the "Generic" power of the function let numericString = "0001234500" let result = numericString.trimming(while: { $0 == "0" }) XCTAssertEqual(result, "12345") From 4be10b6b120f2403386188fd45cd147aa100b98b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 26 Feb 2026 10:30:32 +0100 Subject: [PATCH 3/3] Whoops, wrong package --- Sources/OpenAPIRuntime/Base/ContentDisposition.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Base/ContentDisposition.swift b/Sources/OpenAPIRuntime/Base/ContentDisposition.swift index bab6283..c911c38 100644 --- a/Sources/OpenAPIRuntime/Base/ContentDisposition.swift +++ b/Sources/OpenAPIRuntime/Base/ContentDisposition.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// #if canImport(FoundationEssentials) -import FoundationModels +import FoundationEssentials #else import Foundation #endif