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)
+ }
+}