Skip to content

Commit

Permalink
WIP - new API / adopting swift 6
Browse files Browse the repository at this point in the history
  • Loading branch information
samdeane committed Sep 13, 2024
1 parent 7364f1a commit e193449
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 298 deletions.
16 changes: 16 additions & 0 deletions Extras/Examples/CommandLineExample/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "Logger",
"repositoryURL": "/Users/sam/Developer/Projects/Logger",
"state": {
"branch": "main",
"revision": "a5cfb4ede43a0f4782afb2aea3175a04f206efcf",
"version": null
}
}
]
},
"version": 1
}
82 changes: 40 additions & 42 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
// swift-tools-version:5.6
// swift-tools-version:6.0

import Foundation
import PackageDescription

let package = Package(
name: "Logger",

platforms: [
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v5)
],

products: [
.library(
name: "Logger",
targets: ["Logger"]),
.library(
name: "LoggerUI",
targets: ["LoggerUI"]),
.library(
name: "LoggerTestSupport",
targets: ["LoggerTestSupport"])
],

dependencies: [
],

targets: [
.target(
name: "Logger",
dependencies: []),
.target(
name: "LoggerUI",
dependencies: ["Logger"]),
.target(
name: "LoggerTestSupport",
dependencies: ["Logger"]),
.testTarget(
name: "LoggerTests",
dependencies: ["Logger", "LoggerTestSupport"]),
]
)
name: "Logger",

platforms: [
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v5),
],

products: [
.library(
name: "Logger",
targets: ["Logger"]),
.library(
name: "LoggerUI",
targets: ["LoggerUI"]),
.library(
name: "LoggerTestSupport",
targets: ["LoggerTestSupport"]),
],

dependencies: [],

targets: [
.target(
name: "Logger",
dependencies: []),
.target(
name: "LoggerUI",
dependencies: ["Logger"]),
.target(
name: "LoggerTestSupport",
dependencies: ["Logger"]),
.testTarget(
name: "LoggerTests",
dependencies: ["Logger", "LoggerTestSupport"]),
]
)

import Foundation
if ProcessInfo.processInfo.environment["RESOLVE_COMMAND_PLUGINS"] != nil {
package.dependencies.append(contentsOf: [
.package(url: "https://github.com/elegantchaos/ActionBuilderPlugin.git", from: "1.0.8"),
.package(url: "https://github.com/elegantchaos/SwiftFormatterPlugin.git", from: "1.0.3")
])
package.dependencies.append(contentsOf: [
.package(url: "https://github.com/elegantchaos/ActionBuilderPlugin.git", from: "1.0.8"),
.package(url: "https://github.com/elegantchaos/SwiftFormatterPlugin.git", from: "1.0.3"),
])
}
215 changes: 121 additions & 94 deletions Sources/Logger/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import Foundation
to one or more handlers.
*/

public class Channel: ObservableObject {
/**
public actor Channel {
/**
Default log handler which prints to standard out,
without appending the channel details.
*/

public static let stdoutHandler = PrintHandler("default", showName: false, showSubsystem: false)
public static let stdoutHandler = PrintHandler("default", showName: false, showSubsystem: false)

/**
/**
Default handler to use for channels if nothing else is specified.
On the Mac this is an OSLogHandler, which will log directly to the console without
Expand All @@ -30,17 +30,17 @@ public class Channel: ObservableObject {
On Linux it is a PrintHandler which will log to stdout.
*/

static func initDefaultHandler() -> Handler {
#if os(macOS) || os(iOS)
return OSLogHandler("default")
#else
return stdoutHandler // TODO: should perhaps be stderr instead?
#endif
}
static func initDefaultHandler() -> Handler {
#if os(macOS) || os(iOS)
return OSLogHandler("default")
#else
return stdoutHandler // TODO: should perhaps be stderr instead?
#endif
}

public static let defaultHandler = initDefaultHandler()
public static let defaultHandler = initDefaultHandler()

/**
/**
Default subsystem if nothing else is specified.
If the channel name is in dot syntax (x.y.z), then the last component is
assumed to be the name, and the rest is assumed to be the subsystem.
Expand All @@ -49,9 +49,9 @@ public class Channel: ObservableObject {
is used for the subsytem.
*/

static let defaultSubsystem = "com.elegantchaos.logger"
static let defaultSubsystem = "com.elegantchaos.logger"

/**
/**
Default log channel that clients can use to log their actual output.
This is intended as a convenience for command line programs which actually want to produce output.
They could of course just use print() for this (producing normal output isn't strictly speaking
Expand All @@ -61,46 +61,80 @@ public class Channel: ObservableObject {
Unlike most channels, we want this one to default to always being on.
*/

public static let stdout = Channel("stdout", handlers: [stdoutHandler], alwaysEnabled: true)
public static let stdout = Channel("stdout", handlers: [stdoutHandler], alwaysEnabled: true)

public let name: String
public let subsystem: String

public private(set) var enabled: Bool
public func setEnabled(state: Bool) async {
enabled = state
await ui.setEnabled(state: state)
}

public var fullName: String {
"\(subsystem).\(name)"
}

let manager: Manager
var handlers: [Handler] = []

/// MainActor isolated properties for use in the user interface.
/// This object is observable so the UI can watch it for changes
/// to the enabled state of the channel.
@MainActor public class UI: Identifiable, ObservableObject {
@Published public private(set) var enabled: Bool
public weak var channel: Channel!
public let id: String

public let name: String
public let subsystem: String
@Published public var enabled: Bool
init(channel: Channel, id: String, enabled: Bool) {
self.channel = channel
self.id = id
self.enabled = enabled
}

public var fullName: String {
"\(subsystem).\(name)"
func setEnabled(state: Bool) {
enabled = state
}
}

public let ui: UI

public init(
_ name: String, handlers: @autoclosure () -> [Handler] = [defaultHandler],
alwaysEnabled: Bool = false, manager: Manager = Manager.shared
) {
let components = name.split(separator: ".")
let last = components.count - 1
let shortName: String
let subName: String

if last > 0 {
shortName = String(components[last])
subName = components[..<last].joined(separator: ".")
} else {
shortName = name
subName = Channel.defaultSubsystem
}

let manager: Manager
var handlers: [Handler] = []

public init(_ name: String, handlers: @autoclosure () -> [Handler] = [defaultHandler], alwaysEnabled: Bool = false, manager: Manager = Manager.shared) {
let components = name.split(separator: ".")
let last = components.count - 1
let shortName: String
let subName: String

if last > 0 {
shortName = String(components[last])
subName = components[..<last].joined(separator: ".")
} else {
shortName = name
subName = Channel.defaultSubsystem
}

let fullName = "\(subName).\(shortName)"
let enabledList = manager.channelsEnabledInSettings

self.name = shortName
self.subsystem = subName
self.manager = manager
self.enabled = enabledList.contains(shortName) || enabledList.contains(fullName) || alwaysEnabled
self.handlers = handlers() // TODO: does this need to be a closure any more?

manager.register(channel: self)
let fullName = "\(subName).\(shortName)"
let enabledList = manager.channelsEnabledInSettings
let isEnabled =
enabledList.contains(shortName) || enabledList.contains(fullName) || alwaysEnabled

self.name = shortName
self.subsystem = subName
self.manager = manager
self.enabled = isEnabled

self.handlers = handlers() // TODO: does this need to be a closure any more?
self.ui = UI(channel: self, id: fullName, enabled: isEnabled)
Task {
await manager.register(channel: self)
}
}

/**
/**
Log something.
The logged value is an autoclosure, to avoid doing unnecessary work if the channel is disabled.
Expand All @@ -114,53 +148,46 @@ public class Channel: ObservableObject {
setting.
*/

public func log(_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line, column: UInt = #column, function: StaticString = #function) {
if enabled {
let value = logged()
manager.queue.async {
let context = Context(file: file, line: line, column: column, function: function)
self.handlers.forEach { $0.log(channel: self, context: context, logged: value) }
}
}
}

public func debug(_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line, column: UInt = #column, function: StaticString = #function) {
#if DEBUG
if enabled {
log(logged(), file: file, line: line, column: column, function: function)
}
#endif
}

public func fatal(_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line, column: UInt = #column, function: StaticString = #function) -> Never {
let value = logged()
log(value, file: file, line: line, column: column, function: function)
manager.fatalHandler(value, self, file, line)
public func log(
_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line,
column: UInt = #column, function: StaticString = #function
) {
if enabled {
let value = logged()
let context = Context(file: file, line: line, column: column, function: function)
self.handlers.forEach { $0.log(channel: self, context: context, logged: value) }
}
}

public func debug(
_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line,
column: UInt = #column, function: StaticString = #function
) {
#if DEBUG
if enabled {
log(logged(), file: file, line: line, column: column, function: function)
}
#endif
}

public func fatal(
_ logged: @autoclosure () -> Any, file: StaticString = #file, line: UInt = #line,
column: UInt = #column, function: StaticString = #function
) -> Never {
let value = logged()
log(value, file: file, line: line, column: column, function: function)
manager.fatalHandler(value, self, file, line)
}
}

extension Channel: Hashable {
// For now, we treat channels with the same name as equal,
// as long as they belong to the same manager.
public func hash(into hasher: inout Hasher) {
name.hash(into: &hasher)
}

public static func == (lhs: Channel, rhs: Channel) -> Bool {
(lhs.name == rhs.name) && (lhs.manager === rhs.manager)
}
// For now, we treat channels with the same name as equal,
// as long as they belong to the same manager.
nonisolated public func hash(into hasher: inout Hasher) {
name.hash(into: &hasher)
}

public static func == (lhs: Channel, rhs: Channel) -> Bool {
(lhs.name == rhs.name) && (lhs.manager === rhs.manager)
}
}

extension Channel: Identifiable {
public var id: String { fullName }
}

// MARK: Deprecated API

public extension Channel {
@available(*, deprecated, message: "Use Manager.shared instead")
static var defaultManager: Manager { Manager.shared }
}

@available(*, deprecated, message: "Use ``Channel`` instead.")
public typealias Logger = Channel
Loading

0 comments on commit e193449

Please sign in to comment.