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
38 changes: 31 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import CompilerPluginSupport

// get environment variables
let dynamicLibrary = Context.environment["SWIFT_BUILD_DYNAMIC_LIBRARY"] == "1"
let buildMetadata = Context.environment["SWIFTPM_BLUETOOTH_METADATA"] != "0"
let generateCode = Context.environment["SWIFTPM_ENABLE_PLUGINS"] != "0"
let enableMacros = Context.environment["SWIFTPM_ENABLE_MACROS"] != "0"
let buildDocs = Context.environment["BUILDING_FOR_DOCUMENTATION_GENERATION"] == "1"
Expand Down Expand Up @@ -53,6 +52,23 @@ var package = Package(
targets: ["BluetoothSDP"]
)
],
traits: [
.trait(
name: "Metadata",
description: """
Bluetooth SIG assigned-numbers metadata: the `name` \
properties of `CompanyIdentifier`, `BluetoothUUID` and \
`UnitIdentifier`.

Opt-in because it pulls in Foundation and a resource \
bundle, which makes the package neither Embedded-clean \
nor buildable outside SwiftPM. The numeric definitions \
themselves (`CompanyIdentifier.apple` and friends) are \
generated at build time and remain available without it.
"""
),
.default(enabledTraits: [])
],
targets: [
.target(
name: "Bluetooth"
Expand Down Expand Up @@ -100,7 +116,10 @@ var package = Package(
"Bluetooth",
.target(
name: "BluetoothMetadata",
condition: .when(platforms: [.macOS, .linux, .macCatalyst, .windows])
condition: .when(
platforms: [.macOS, .linux, .macCatalyst, .windows],
traits: ["Metadata"]
)
),
.target(
name: "BluetoothGAP",
Expand All @@ -124,11 +143,16 @@ var package = Package(
)

// Optional dependencies
if buildMetadata {
package.targets[0].dependencies += [
"BluetoothMetadata"
]
}
//
// The metadata tables are reached only through `canImport`-guarded code
// in `Sources/Bluetooth`, so conditioning the dependency on the trait is
// enough to compile them out.
package.targets[0].dependencies += [
.target(
name: "BluetoothMetadata",
condition: .when(traits: ["Metadata"])
)
]

if embeddedFoundation {
package.dependencies += [
Expand Down
4 changes: 2 additions & 2 deletions Sources/Bluetooth/BluetoothUUID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ extension BluetoothUUID: Equatable {
extension BluetoothUUID: CustomStringConvertible {

public var description: String {
#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
if let name = self.metadata?.name {
return "\(rawValue) (\(name))"
} else {
Expand All @@ -73,7 +73,7 @@ extension BluetoothUUID: CustomStringConvertible {
extension BluetoothUUID: LosslessStringConvertible {

public init?(_ string: String) {
#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
var rawValue = string
var name: String?
// Find UUID name
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bluetooth/BluetoothUUIDMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Alsey Coleman Miller on 1/12/25.
//

#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bluetooth/CompanyIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ extension CompanyIdentifier: ExpressibleByIntegerLiteral {
extension CompanyIdentifier: CustomStringConvertible {

public var description: String {
#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
return name ?? rawValue.description
#else
return rawValue.description
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bluetooth/CompanyIdentifierMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Alsey Coleman Miller on 1/12/25.
//

#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bluetooth/UnitIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension UnitIdentifier: ExpressibleByIntegerLiteral {
extension UnitIdentifier: CustomStringConvertible {

public var description: String {
#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
return self.name ?? rawValueDescription
#else
return rawValueDescription
Expand Down
2 changes: 1 addition & 1 deletion Sources/Bluetooth/UnitIdentifierMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Alsey Coleman Miller on 1/12/25.
//

#if canImport(Foundation) && canImport(BluetoothMetadata) && !os(WASI) && !hasFeature(Embedded)
#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
Expand Down
6 changes: 5 additions & 1 deletion Tests/BluetoothTests/BluetoothTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ import BluetoothGATT

let company: CompanyIdentifier = 76 // Apple, Inc.

#if !os(WASI)
// The description falls back to the raw value without the
// assigned-numbers metadata the Metadata trait gates.
#if Metadata && !os(WASI)
#expect(company.description == "Apple, Inc.")
#else
#expect(company.description == "76")
#endif
#expect(company.hashValue != 0)
#expect(company != 77)
Expand Down
8 changes: 7 additions & 1 deletion Tests/BluetoothTests/BluetoothUUIDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,14 @@ import Bluetooth

#if !os(WASI)
#expect(uuid == BluetoothUUID.Member.savantSystems2)
#endif
// The name, and the description that embeds it, come from the
// assigned-numbers metadata, which the Metadata trait gates.
#if Metadata && !os(WASI)
#expect(uuid.metadata?.name == "Savant Systems LLC")
#expect("\(uuid)" == "FEA9 (Savant Systems LLC)")
#else
#expect("\(uuid)" == uuidString)
#endif
#expect(uuid.rawValue == uuidString)
#expect(uuid.hashValue != 0)
Expand Down Expand Up @@ -198,7 +204,7 @@ import Bluetooth

let uuid = try #require(BluetoothUUID(rawValue: uuidString))

#if !os(WASI)
#if Metadata && !os(WASI)
#expect(uuid.metadata == nil)
#endif
#expect(uuid.rawValue == uuidString)
Expand Down
12 changes: 12 additions & 0 deletions Tests/BluetoothTests/GAPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,13 @@ import Bluetooth
let uuidList: GAPCompleteListOf16BitServiceClassUUIDs = [0x1803, 0x1804, 0x1802]
let localName: GAPCompleteLocalName = "Proximity"

// The names come from the assigned-numbers metadata gated by
// the Metadata trait; without it the description is raw values.
#if Metadata
#expect(uuidList.description == "[1803 (Link Loss), 1804 (Tx Power), 1802 (Immediate Alert)]")
#else
#expect(uuidList.description == "[1803, 1804, 1802]")
#endif

let expectedData: [GAPData] = [flags, uuidList, localName]
let types = expectedData.map { type(of: $0) }
Expand Down Expand Up @@ -219,7 +225,13 @@ import Bluetooth
let uuidList: GAPIncompleteListOf16BitServiceClassUUIDs = [0x1803, 0x1804, 0x1802]
let localName: GAPCompleteLocalName = "Proximity"

// The names come from the assigned-numbers metadata gated by
// the Metadata trait; without it the description is raw values.
#if Metadata
#expect(uuidList.description == "[1803 (Link Loss), 1804 (Tx Power), 1802 (Immediate Alert)]")
#else
#expect(uuidList.description == "[1803, 1804, 1802]")
#endif

let expectedData: [GAPData] = [flags, uuidList, localName]
let types = expectedData.map { type(of: $0) }
Expand Down
5 changes: 5 additions & 0 deletions Tests/BluetoothTests/GATTCharacteristicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,14 @@ import Bluetooth
(101...UInt8.max).forEach { #expect(GATTBatteryLevel(data: Data([$0])) == nil) }

// test percentage
//
// `name`, `type`, and the description that falls back to `name`,
// come from the assigned-numbers metadata gated by the Metadata trait.
#if Metadata
#expect(GATTBatteryPercentage.unitType.description == "percentage")
#expect(GATTBatteryPercentage.unitType.name == "percentage")
#expect(GATTBatteryPercentage.unitType.type == "org.bluetooth.unit.percentage")
#endif
#expect(GATTBatteryPercentage.unitType == .percentage)
(0...100).forEach { #expect(GATTBatteryPercentage(rawValue: $0) != nil) }
(101...UInt8.max).forEach { #expect(GATTBatteryPercentage(rawValue: $0) == nil) }
Expand Down
6 changes: 6 additions & 0 deletions Tests/BluetoothTests/HCITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,13 @@ import Foundation
#expect(localVersionInformation.lmpVersion == 0x08)
#expect(localVersionInformation.lmpSubversion == 0x219A)
#expect(localVersionInformation.manufacturer.rawValue == 0x000F)
// The description falls back to the raw value without the
// assigned-numbers metadata the Metadata trait gates.
#if Metadata
#expect(localVersionInformation.manufacturer.description == "Broadcom Corporation")
#else
#expect(localVersionInformation.manufacturer.description == "15")
#endif
}

@Test func readDeviceAddress() async throws {
Expand Down
Loading