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
18 changes: 12 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,18 @@ if generateCode {
]
)
]
package.targets[0].plugins = [
"GenerateBluetoothDefinitions"
]
package.targets[4].plugins = [
"GenerateBluetoothDefinitions"
]
// Name-based lookup, not a hardcoded index — the target array's
// order has changed before (silently detaching this plugin from
// BluetoothTests when a new target was inserted ahead of it) and
// will again.
for name in ["Bluetooth", "BluetoothTests"] {
guard let index = package.targets.firstIndex(where: { $0.name == name }) else {
fatalError("Missing target \(name)")
}
package.targets[index].plugins = [
"GenerateBluetoothDefinitions"
]
}
}

if enableMacros {
Expand Down
38 changes: 38 additions & 0 deletions Plugins/GenerateBluetoothDefinitions/BluetoothUUIDMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// BluetoothUUIDMetadata.swift
// Bluetooth
//
// Created by Alsey Coleman Miller on 1/12/25.
//

import Foundation
import PackagePlugin

extension GenerateBluetoothDefinitionsPlugin {

func bluetoothUUIDMetadataBuildCommands(
for context: PluginContext,
target: SwiftSourceModuleTarget,
commands: inout [Command]
) throws {
guard target.name == "Bluetooth" else {
return
}
// Generate Bluetooth UUID Metadata (name/type lookup, merged
// across all categories)
let outputDirectory = context.pluginWorkDirectoryURL
let outputURLs = [
outputDirectory.appending(component: "BluetoothUUIDMetadataNames.swift")
]
let arguments = ["uuidMetadata"] + outputURLs.map { $0.path() }
let command = Command.buildCommand(
displayName: "Generate Bluetooth UUID Metadata",
executable: try context.tool(named: "GenerateBluetooth").url,
arguments: arguments,
inputFiles: [],
outputFiles: outputURLs
)
assert(arguments.count == 2)
commands.append(command)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// CompanyIdentifierMetadata.swift
// Bluetooth
//
// Created by Alsey Coleman Miller on 1/12/25.
//

import Foundation
import PackagePlugin

extension GenerateBluetoothDefinitionsPlugin {

func companyIdentifierMetadataBuildCommands(
for context: PluginContext,
target: SwiftSourceModuleTarget,
commands: inout [Command]
) throws {
guard target.name == "Bluetooth" else {
return
}
// Generate Bluetooth Company Identifier Metadata (name lookup)
let outputDirectory = context.pluginWorkDirectoryURL
let outputURLs = [
outputDirectory.appending(component: "CompanyIdentifierNames.swift")
]
let arguments = ["companyIdentifierMetadata"] + outputURLs.map { $0.path() }
let command = Command.buildCommand(
displayName: "Generate Bluetooth Company Identifier Metadata",
executable: try context.tool(named: "GenerateBluetooth").url,
arguments: arguments,
inputFiles: [],
outputFiles: outputURLs
)
assert(arguments.count == 2)
commands.append(command)
}
}
4 changes: 4 additions & 0 deletions Plugins/GenerateBluetoothDefinitions/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct GenerateBluetoothDefinitionsPlugin: BuildToolPlugin {

// Generate Bluetooth Company Identifier Definitions
try companyIdentifiersBuildCommands(for: context, target: target, commands: &commands)
// Generate Bluetooth Company Identifier Metadata (name lookup)
try companyIdentifierMetadataBuildCommands(for: context, target: target, commands: &commands)
// Generate Bluetooth Company Identifier Unit Tests
try companyIdentifierTestsBuildCommands(for: context, target: target, commands: &commands)
// Generate Bluetooth Unit Identifier Definitions
Expand All @@ -44,6 +46,8 @@ struct GenerateBluetoothDefinitionsPlugin: BuildToolPlugin {
commands: &commands
)
}
// Generate Bluetooth UUID Metadata (name/type lookup)
try bluetoothUUIDMetadataBuildCommands(for: context, target: target, commands: &commands)

return commands
}
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 Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if Metadata
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 Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if Metadata
var rawValue = string
var name: String?
// Find UUID name
Expand Down
47 changes: 23 additions & 24 deletions Sources/Bluetooth/BluetoothUUIDMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,36 @@
// Created by Alsey Coleman Miller on 1/12/25.
//

#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#if Metadata
public extension BluetoothUUID {

/// Bluetooth SIG assigned-numbers metadata for a 16-bit UUID.
struct Metadata: Equatable, Hashable, Sendable {

/// The name of the defined UUID.
public let name: String

/// The Bluetooth type namespace of the UUID.
public let type: String?
}
}

#if !hasFeature(Embedded)
extension BluetoothUUID.Metadata: Codable {}
#endif
import BluetoothMetadata

public extension BluetoothUUID {

/// Fetch the metadata for the UUID.
var metadata: BluetoothMetadata.BluetoothUUID? {
guard case let .bit16(rawValue) = self else {
///
/// - SeeAlso: [Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/)
var metadata: Metadata? {
guard case let .bit16(rawValue) = self,
let generated = Self.generatedMetadata(for: rawValue)
else {
return nil
}
for file in files.values {
if let metadata = file[rawValue] {
return metadata
}
}
return nil
return Metadata(name: generated.name, type: generated.type)
}
}

internal let files: [BluetoothMetadata.BluetoothUUID.Category: BluetoothMetadata.BluetoothUUID.File] = {
do {
return try BluetoothMetadata.BluetoothUUID.File.load()
} catch {
assertionFailure("Unable to load metadata: \(error)")
return [:]
}
}()

#endif
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 Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if Metadata
return name ?? rawValue.description
#else
return rawValue.description
Expand Down
26 changes: 2 additions & 24 deletions Sources/Bluetooth/CompanyIdentifierMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,14 @@
// Created by Alsey Coleman Miller on 1/12/25.
//

#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import BluetoothMetadata

#if Metadata
public extension CompanyIdentifier {

/// Bluetooth Company name.
///
/// - SeeAlso: [Company Identifiers](https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers)
var name: String? {
companyIdentifiers[rawValue]
Self.generatedName(for: rawValue)
}
}

internal let companyIdentifiers: [UInt16: String] = {
do {
let file = try BluetoothMetadata.CompanyIdentifier.File.load()
var companyIdentifiers = [UInt16: String]()
for element in file.companyIdentifiers {
companyIdentifiers[element.id] = element.name
}
assert(companyIdentifiers.count == file.companyIdentifiers.count)
return companyIdentifiers
} catch {
assertionFailure("Unable to load metadata: \(error)")
return [:]
}
}()
#endif
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 Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if Metadata
return self.name ?? rawValueDescription
#else
return rawValueDescription
Expand Down
9 changes: 1 addition & 8 deletions Sources/Bluetooth/UnitIdentifierMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@
// Created by Alsey Coleman Miller on 1/12/25.
//

#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import BluetoothMetadata

#if Metadata
public extension UnitIdentifier {

/// The name of the unit.
Expand Down
79 changes: 79 additions & 0 deletions Sources/GenerateBluetooth/BluetoothUUIDMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// BluetoothUUIDMetadata.swift
//
//
// Created by Alsey Coleman Miller on 1/12/25.
//

import Foundation
import BluetoothMetadata

extension GenerateTool {

/// Categories merged in this fixed order when the same 16-bit value
/// appears in more than one — matches `BluetoothUUIDMetadata.swift`'s
/// prior runtime behavior of returning the first match found, but
/// deterministically instead of in unspecified dictionary order.
static var uuidMetadataCategoryOrder: [BluetoothMetadata.BluetoothUUID.Category] {
[.service, .characteristic, .declaration, .descriptor, .member, .unit]
}

static func generateBluetoothUUIDMetadata(output: URL) throws {
var merged = [UInt16: BluetoothMetadata.BluetoothUUID]()
for category in uuidMetadataCategoryOrder {
let file = try BluetoothMetadata.BluetoothUUID.File(category)
for entry in file.uuids {
if let existing = merged[entry.uuid] {
print(
"warning: BluetoothUUID 0x\(entry.uuid.toHexadecimal()) is defined in more than one category"
+ " (keeping \"\(existing.name)\", ignoring \"\(entry.name)\" from \(category))"
)
continue
}
merged[entry.uuid] = entry
}
}
try generateBluetoothUUIDMetadataSwitch(merged, output: output)
}

static func generateBluetoothUUIDMetadataSwitch(
_ data: [UInt16: BluetoothMetadata.BluetoothUUID],
output: URL
) throws {

var generatedCode = ""
let entries = data.values.sorted(by: { $0.uuid < $1.uuid })

func 🖨(_ text: String) {
generatedCode += text + "\n"
}

🖨("//")
🖨("// \(output.lastPathComponent)")
🖨("// Bluetooth")
🖨("//")
🖨("// Generated from Resources/{Service,Characteristic,Declaration,Descriptor,Member,Unit}UUID.json — do not edit.")
🖨("//")
🖨("")
🖨("#if Metadata")
🖨("extension BluetoothUUID {")
🖨("")
🖨(" // swiftlint:disable:next cyclomatic_complexity")
🖨(" static func generatedMetadata(for rawValue: UInt16) -> (name: String, type: String?)? {")
🖨(" switch rawValue {")

for entry in entries {
let typeLiteral = entry.type.map { "#\"\($0)\"#" } ?? "nil"
🖨(" case \(entry.uuid): return (#\"\(entry.name)\"#, \(typeLiteral))")
}

🖨(" default: return nil")
🖨(" }")
🖨(" }")
🖨("}")
🖨("#endif")

try generatedCode.write(toFile: output.path, atomically: true, encoding: .utf8)
print("Generated \(output.path)")
}
}
Loading
Loading