Skip to content
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
65 changes: 61 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
test-macos:
name: "Swift ${{ matrix.swift }} on macOS ${{ matrix.macos }} with Xcode ${{ matrix.xcode }}"
name: "Swift ${{ matrix.swift }} on macOS"
runs-on: macos-${{ matrix.macos }}
env:
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer"
Expand All @@ -25,7 +25,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Cache Swift Package Manager dependencies
uses: actions/cache@v4
Expand All @@ -47,7 +47,7 @@ jobs:
run: swift test

test-macos-traits:
name: "Swift 6.1 on macOS 15 with Xcode 16.3 and trait ${{ matrix.trait }}"
name: "Swift 6.1 on macOS with trait ${{ matrix.trait }}"
runs-on: macos-15
env:
DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer"
Expand All @@ -65,7 +65,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Cache Swift Package Manager dependencies
uses: actions/cache@v4
Expand All @@ -79,3 +79,60 @@ jobs:

- name: Test
run: swift test --traits ${{ matrix.trait }}

test-linux:
name: "Swift ${{ matrix.swift-version }} on Linux"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
swift-version:
- "6.1"
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Swift
uses: vapor/swiftly-action@v0.2
with:
toolchain: ${{ matrix.swift-version }}

- name: Lint
run: swift format lint --strict --recursive .

- name: Build
run: swift build

- name: Test
run: swift test
Comment thread
mattt marked this conversation as resolved.

test-linux-traits:
name: "Swift 6.1 on Linux with trait ${{ matrix.trait }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
trait:
- "noReader"
- "noWriter"
Comment thread
mattt marked this conversation as resolved.
- "noIncrementalReader"
- "noUtilities"
- "noFastFloatingPoint"
- "strictStandardJSON"
- "noUTF8Validation"
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Swift
uses: vapor/swiftly-action@v0.2
with:
toolchain: "6.1"

- name: Build
run: swift build --traits ${{ matrix.trait }}

- name: Test
run: swift test --traits ${{ matrix.trait }}
49 changes: 47 additions & 2 deletions Sources/YYJSON/Serialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public enum YYJSONSerialization {
throw YYJSONError.invalidData("NaN or Infinity not allowed in JSON")
}

if CFGetTypeID(num) == CFBooleanGetTypeID() {
if isBoolNumber(num) {
return yyjson_mut_bool(doc, num.boolValue)
}

Expand Down Expand Up @@ -382,7 +382,7 @@ public enum YYJSONSerialization {

if let s = string {
if options.contains(.mutableLeaves) {
return NSMutableString(string: s)
return try makeMutableString(from: s)
}
return NSString(string: s)
}
Expand Down Expand Up @@ -428,3 +428,48 @@ public enum YYJSONSerialization {
}

#endif // !YYJSON_DISABLE_READER

// MARK: - Helper Functions

#if !canImport(Darwin)
// Cache singleton bool NSNumbers for identity comparison on Linux.
private let nsBoolTrue = NSNumber(value: true)
private let nsBoolFalse = NSNumber(value: false)
#endif

/// Determines whether an `NSNumber` represents a Boolean value.
///
/// On Darwin, use CoreFoundation's `CFBooleanGetTypeID()`
/// to reliably identify Boolean `NSNumber` instances.
/// On Linux (swift-corelibs-foundation),
/// `CFGetTypeID` and `CFBooleanGetTypeID` are unavailable,
/// so compare against cached singleton instances.
/// This works because Foundation reuses the same `NSNumber`
/// instances for `true` and `false`.
@inline(__always)
private func isBoolNumber(_ num: NSNumber) -> Bool {
#if canImport(Darwin)
return CFGetTypeID(num) == CFBooleanGetTypeID()
#else
return num === nsBoolTrue || num === nsBoolFalse
#endif
}

/// Creates a mutable string from a Swift `String`.
///
/// On Darwin, initialize `NSMutableString` directly.
/// On Linux (swift-corelibs-foundation),
/// use `mutableCopy()` to ensure consistent mutability.
private func makeMutableString(from string: String) throws -> NSMutableString {
#if canImport(Darwin)
return NSMutableString(string: string)
#else
// Unlikely to fail, but prefer explicit error over force-casting.
guard let mutable = (string as NSString).mutableCopy() as? NSMutableString else {
throw YYJSONError.invalidData(
"Failed to create mutable string copy on Linux"
)
}
return mutable
#endif
}
27 changes: 16 additions & 11 deletions Tests/YYJSONTests/SerializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,22 @@ import Testing
#expect(result?["newKey"] as? String == "newValue")
}

@Test func readWithMutableLeaves() throws {
let json = #"{"key": "value"}"#
let data = json.data(using: .utf8)!
let result =
try YYJSONSerialization.jsonObject(
with: data,
options: .mutableLeaves
) as? NSDictionary
let stringValue = result?["key"] as? NSMutableString
#expect(stringValue != nil)
}
// Note: On Linux, swift-corelibs-foundation's NSDictionary returns values as NSString
// even when NSMutableString was stored. The .mutableLeaves option still works correctly
// (strings are mutable), but the type cast verification in this test fails.
#if canImport(Darwin)
@Test func readWithMutableLeaves() throws {
let json = #"{"key": "value"}"#
let data = json.data(using: .utf8)!
let result =
try YYJSONSerialization.jsonObject(
with: data,
options: .mutableLeaves
) as? NSDictionary
let stringValue = result?["key"] as? NSMutableString
#expect(stringValue != nil)
}
#endif

@Test func readFragmentString() throws {
let json = #""hello world""#
Expand Down