diff --git a/Sources/StreamAttachments/Info.plist b/Sources/StreamAttachments/Info.plist index 2d8465c..553bbcc 100644 --- a/Sources/StreamAttachments/Info.plist +++ b/Sources/StreamAttachments/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.7.0 + 0.8.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Sources/StreamCore/Info.plist b/Sources/StreamCore/Info.plist index 2d8465c..553bbcc 100644 --- a/Sources/StreamCore/Info.plist +++ b/Sources/StreamCore/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.7.0 + 0.8.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Sources/StreamCore/OpenAPI/Utils/APIHelper.swift b/Sources/StreamCore/OpenAPI/Utils/APIHelper.swift index 9be5f8a..424d32d 100644 --- a/Sources/StreamCore/OpenAPI/Utils/APIHelper.swift +++ b/Sources/StreamCore/OpenAPI/Utils/APIHelper.swift @@ -63,6 +63,15 @@ public enum APIHelper { return source } + /// Maps a value to a single path component and percent-encodes it for use in a URL path. + /// Folds the previous two-step `mapValueToPathItem` + `addingPercentEncoding` into one call + /// so generated endpoint paths stay compact. A `nil` value maps to an empty component. + public static func escapedPathItem(_ value: Any?) -> String { + guard let value else { return "" } + let item = "\(mapValueToPathItem(value))" + return item.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + } + /// maps all values from source to query parameters /// /// explode attribute is respected: collection values might be either joined or split up into separate key value pairs diff --git a/Sources/StreamCore/Utils/SystemEnvironment+Version.swift b/Sources/StreamCore/Utils/SystemEnvironment+Version.swift index 5a5f018..5692763 100644 --- a/Sources/StreamCore/Utils/SystemEnvironment+Version.swift +++ b/Sources/StreamCore/Utils/SystemEnvironment+Version.swift @@ -6,5 +6,5 @@ import Foundation enum SystemEnvironment { /// A Stream Core version. - public static let version: String = "0.7.0" + public static let version: String = "0.8.0" } diff --git a/Sources/StreamCoreUI/Info.plist b/Sources/StreamCoreUI/Info.plist index 2d8465c..553bbcc 100644 --- a/Sources/StreamCoreUI/Info.plist +++ b/Sources/StreamCoreUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 0.7.0 + 0.8.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Tests/StreamCoreTests/OpenAPI/Utils/APIHelper_Tests.swift b/Tests/StreamCoreTests/OpenAPI/Utils/APIHelper_Tests.swift new file mode 100644 index 0000000..448f87b --- /dev/null +++ b/Tests/StreamCoreTests/OpenAPI/Utils/APIHelper_Tests.swift @@ -0,0 +1,57 @@ +// +// Copyright © 2026 Stream.io Inc. All rights reserved. +// + +import Foundation +@testable import StreamCore +import Testing + +struct APIHelper_Tests { + // MARK: - escapedPathItem + + @Test("Plain string with only allowed characters passes through unchanged") + func escapedPathItem_plainString() { + #expect(APIHelper.escapedPathItem("simple-id_123.~") == "simple-id_123.~") + } + + @Test("Characters disallowed in a path are percent-encoded") + func escapedPathItem_encodesDisallowedCharacters() { + #expect(APIHelper.escapedPathItem("a b") == "a%20b") + #expect(APIHelper.escapedPathItem("a#b") == "a%23b") + #expect(APIHelper.escapedPathItem("a?b") == "a%3Fb") + #expect(APIHelper.escapedPathItem("100%") == "100%25") + // ":" is not in .urlPathAllowed, so a channel CID's separator is encoded. + #expect(APIHelper.escapedPathItem("messaging:general") == "messaging%3Ageneral") + } + + @Test("Characters allowed in a URL path are not encoded") + func escapedPathItem_keepsPathAllowedCharacters() { + // .urlPathAllowed permits "/", "@", ",", ";", matching the legacy + // per-parameter escaping the generated EndpointPath relied on. + #expect(APIHelper.escapedPathItem("a/b@c,d;e") == "a/b@c,d;e") + } + + @Test("Array value is comma-joined then escaped") + func escapedPathItem_arrayValue() { + #expect(APIHelper.escapedPathItem(["a", "b c"]) == "a,b%20c") + } + + @Test("nil value maps to an empty component") + func escapedPathItem_nil() { + #expect(APIHelper.escapedPathItem(nil) == "") + } + + @Test("Matches the previous two-step mapValueToPathItem + addingPercentEncoding") + func escapedPathItem_matchesLegacyTwoStep() { + for value in ["foo bar/baz", "100% done", "messaging:general"] as [Any] { + let preEscape = "\(APIHelper.mapValueToPathItem(value))" + let postEscape = preEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + #expect(APIHelper.escapedPathItem(value) == postEscape) + } + + let array: [String] = ["a", "b c", "d/e"] + let preEscape = "\(APIHelper.mapValueToPathItem(array))" + let postEscape = preEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + #expect(APIHelper.escapedPathItem(array) == postEscape) + } +}