diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2680e910..a3816d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,12 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Set up Swift - uses: swift-actions/setup-swift@v2 - with: - swift-version: '6.0' + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: Build - run: swift build --build-tests + run: xcrun swift build --build-tests - name: Run tests - run: swift test \ No newline at end of file + run: xcrun swift test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0935d3..082b0247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [Full Changelog](https://github.com/SwiftyJSON/SwiftyJSON/compare/2.2.0...HEAD) +### Added +- Swift 6 support with full Sendable conformance ([#1163](https://github.com/SwiftyJSON/SwiftyJSON/issues/1163)) +- Comprehensive concurrency tests for actor boundary validation + +### Changed +- `JSON` struct conforms to `@unchecked Sendable` +- `Type`, `SwiftyJSONError`, `JSONKey` enums conform to `Sendable` +- Minimum Swift tools version updated to 6.0 + **Closed issues:** - 156 compiler errors Mavericks + Xcode 6.2 [\#220](https://github.com/SwiftyJSON/SwiftyJSON/issues/220) diff --git a/Package.swift b/Package.swift index ba89fb5f..fb026e55 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -20,6 +20,5 @@ let package = Package( .copy("Tests.json") ] ) - ], - swiftLanguageVersions: [.v5] + ] ) diff --git a/README.md b/README.md index e1740680..a6f3037a 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ if let userName = result.string { ## Requirements - iOS 8.0+ | macOS 10.10+ | tvOS 9.0+ | watchOS 2.0+ -- Xcode 8 +- Xcode 16+ +- Swift 6.0+ ## Integration diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 97daf4fc..d2f39ef2 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -24,7 +24,7 @@ import Foundation // MARK: - Error // swiftlint:disable line_length -public enum SwiftyJSONError: Int, Swift.Error { +public enum SwiftyJSONError: Int, Swift.Error, Sendable { case unsupportedType = 999 case indexOutOfBounds = 900 case elementTooDeep = 902 @@ -67,7 +67,7 @@ JSON's type definitions. See http://www.json.org */ -public enum Type: Int { +public enum Type: Int, Sendable { case number case string case bool @@ -79,7 +79,7 @@ public enum Type: Int { // MARK: - JSON Base -public struct JSON { +public struct JSON: @unchecked Sendable { /** Creates a JSON using the data. @@ -339,7 +339,7 @@ extension JSON: Swift.Collection { /** * To mark both String and Int can be used in subscript. */ -public enum JSONKey { +public enum JSONKey: Sendable { case index(Int) case key(String) } diff --git a/Tests/SwiftJSONTests/ConcurrencyTests.swift b/Tests/SwiftJSONTests/ConcurrencyTests.swift new file mode 100644 index 00000000..a6eaa099 --- /dev/null +++ b/Tests/SwiftJSONTests/ConcurrencyTests.swift @@ -0,0 +1,89 @@ +// ConcurrencyTests.swift +// +// Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import SwiftyJSON + +/// Tests verifying Sendable conformance for Swift 6 concurrency. +/// Each test targets a specific type made Sendable in this PR. +class ConcurrencyTests: XCTestCase { + + actor Processor { + func extract(_ json: JSON) -> String { + json["name"].stringValue + } + + func extractType(_ json: JSON) -> Type { + json["value"].type + } + + func requireField(_ json: JSON) throws -> String { + guard json["required"].exists() else { + throw SwiftyJSONError.notExist + } + return json["required"].stringValue + } + } + + /// Verifies JSON conforms to Sendable (can cross actor boundary) + func testJSONSendable() async { + let json = JSON(["name": "test", "count": 42]) + let processor = Processor() + + let result = await processor.extract(json) + + XCTAssertEqual(result, "test") + } + + /// Verifies Type enum conforms to Sendable (can be returned across actor boundary) + func testTypeSendable() async { + let processor = Processor() + + let stringType = await processor.extractType(JSON(["value": "hello"])) + let numberType = await processor.extractType(JSON(["value": 123])) + let boolType = await processor.extractType(JSON(["value": true])) + let nullType = await processor.extractType(JSON(["value": NSNull()])) + let arrayType = await processor.extractType(JSON(["value": [1, 2, 3]])) + let dictType = await processor.extractType(JSON(["value": ["nested": "object"]])) + + XCTAssertEqual(stringType, .string) + XCTAssertEqual(numberType, .number) + XCTAssertEqual(boolType, .bool) + XCTAssertEqual(nullType, .null) + XCTAssertEqual(arrayType, .array) + XCTAssertEqual(dictType, .dictionary) + } + + /// Verifies SwiftyJSONError conforms to Sendable (can be thrown across actor boundary) + func testSwiftyJSONErrorSendable() async { + let processor = Processor() + + do { + _ = try await processor.requireField(JSON(["other": "value"])) + XCTFail("Should have thrown") + } catch SwiftyJSONError.notExist { + // Error successfully crossed actor boundary + } catch { + XCTFail("Unexpected error: \(error)") + } + } +}