diff --git a/Sources/ConsoleLogger/ConsoleLogger+bootstrap.swift b/Sources/ConsoleLogger/ConsoleLogger+bootstrap.swift index 537d63f..f3e733e 100644 --- a/Sources/ConsoleLogger/ConsoleLogger+bootstrap.swift +++ b/Sources/ConsoleLogger/ConsoleLogger+bootstrap.swift @@ -44,12 +44,13 @@ extension ConsoleLogger { /// - metadata: Extra metadata to log with all messages. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to bootstrap the logging system with. /// - fragment: The logger fragment which will be used to build the logged messages. + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) public static func bootstrap( printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), level: Logger.Level = .info, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil, - @LoggerFragmentBuilder fragment: () -> T + @LoggerFragmentBuilder<0> fragment: () -> T ) { self.bootstrap(fragment: fragment(), printer: printer, level: level, metadata: metadata, metadataProvider: metadataProvider) } @@ -102,12 +103,13 @@ extension ConsoleLogger { /// - metadata: Extra metadata to log with all messages. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to bootstrap the logging system with. /// - fragment: The logger fragment which will be used to build the logged messages. + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) public static func bootstrapWithConfigReader( printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), config: ConfigReader = ConfigReader(providers: [CommandLineArgumentsProvider(), EnvironmentVariablesProvider()]), metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil, - @LoggerFragmentBuilder fragment: () -> T + @LoggerFragmentBuilder<0> fragment: () -> T ) { self.bootstrapWithConfigReader( fragment: fragment(), diff --git a/Sources/ConsoleLogger/ConsoleLogger.swift b/Sources/ConsoleLogger/ConsoleLogger.swift index f6250d6..11b0442 100644 --- a/Sources/ConsoleLogger/ConsoleLogger.swift +++ b/Sources/ConsoleLogger/ConsoleLogger.swift @@ -57,13 +57,14 @@ public struct ConsoleLogger: LogHandler, Sendable { /// - metadata: Extra metadata to log with the message. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to use for this logger. This defaults to `nil`. /// - fragment: The ``LoggerFragment`` this logger outputs through. + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) public init( printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), label: String, level: Logger.Level = .debug, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil, - @LoggerFragmentBuilder fragment: () -> T + @LoggerFragmentBuilder<0> fragment: () -> T ) { self.fragment = fragment() self.printer = printer @@ -114,13 +115,14 @@ public struct ConsoleLogger: LogHandler, Sendable { /// - metadata: Extra metadata to log with the message. This defaults to an empty dictionary. /// - metadataProvider: The metadata provider to use for this logger. This defaults to `nil`. /// - fragment: The ``LoggerFragment`` this logger outputs through. + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) public init( printer: any ConsoleLoggerPrinter = DefaultConsoleLoggerPrinter(), label: String, config: ConfigReader, metadata: Logger.Metadata = [:], metadataProvider: Logger.MetadataProvider? = nil, - @LoggerFragmentBuilder fragment: () -> T + @LoggerFragmentBuilder<0> fragment: () -> T ) { self.fragment = fragment() self.printer = printer diff --git a/Sources/ConsoleLogger/Docs.docc/index.md b/Sources/ConsoleLogger/Docs.docc/index.md index ae36c72..6632a75 100644 --- a/Sources/ConsoleLogger/Docs.docc/index.md +++ b/Sources/ConsoleLogger/Docs.docc/index.md @@ -22,7 +22,6 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console. - ``LoggerFragment`` - ``LoggerFragmentBuilder`` -- ``LoggerSpacedFragmentBuilder`` - ``FragmentOutput`` - ``IfMaxLevelFragment`` - ``AndFragment`` diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift index 8d7c28d..1eb8c79 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift @@ -238,8 +238,10 @@ public struct SeparatorFragment: LoggerFragment { public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { if output.needsSeparator { if self.fragment.hasContent(record: &record) { - output.needsSeparator = false - output += self.literal + if !self.literal.isEmpty { + output.needsSeparator = false + output += self.literal + } } } @@ -352,10 +354,11 @@ public struct TimestampFragment: LoggerFragment { } /// A fragment that wraps another fragment, automatically separating its components with spaces. +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) public struct SpacedFragment: LoggerFragment { public let fragment: T - public init(@LoggerSpacedFragmentBuilder _ content: () -> T) { + public init(@LoggerFragmentBuilder<1> _ content: () -> T) { self.fragment = content() } diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift index 2d73bbd..02c1df7 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragmentBuilder.swift @@ -1,8 +1,13 @@ /// A result builder for creating logger fragments in a declarative way. /// /// This allows you to build complex logger fragment combinations using Swift's result builder syntax. +/// +/// You can add spaces between fragments by specifying the number of spaces as the generic parameter. +/// For example, `@LoggerFragmentBuilder<1>` will add a single space between fragments, +/// while `@LoggerFragmentBuilder<0>` will not add any spaces. +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @resultBuilder -public enum LoggerFragmentBuilder { +public enum LoggerFragmentBuilder { /// Build an expression from a single logger fragment. public static func buildExpression(_ fragment: F) -> F { fragment @@ -32,8 +37,8 @@ public enum LoggerFragmentBuilder { public static func buildPartialBlock( accumulated: F1, next: F2 - ) -> AndFragment { - AndFragment(accumulated, next) + ) -> AndFragment> { + AndFragment(accumulated, next.separated(String(repeating: " ", count: spaces))) } /// Handle optional fragments using an optional wrapper. @@ -53,6 +58,6 @@ public enum LoggerFragmentBuilder { /// Build an array of fragments using a custom ``ArrayFragment``. public static func buildArray(_ fragments: [F]) -> ArrayFragment { - ArrayFragment(fragments) + ArrayFragment(fragments, separator: String(repeating: " ", count: spaces)) } } diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift deleted file mode 100644 index fbb5ccd..0000000 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerSpacedFragmentBuilder.swift +++ /dev/null @@ -1,59 +0,0 @@ -/// A result builder for creating logger fragments in a declarative way. -/// -/// This allows you to build complex logger fragment combinations using Swift's result builder syntax. -/// Like ``LoggerFragmentBuilder``, but automatically separates fragments with a space. -@resultBuilder -public enum LoggerSpacedFragmentBuilder { - /// Build an expression from a single logger fragment. - public static func buildExpression(_ fragment: F) -> F { - LoggerFragmentBuilder.buildExpression(fragment) - } - - /// Build an expression from a string literal, creating a ``LiteralFragment``. - public static func buildExpression(_ literal: String) -> LiteralFragment { - LoggerFragmentBuilder.buildExpression(literal) - } - - /// Build a block from a single logger fragment. - public static func buildBlock(_ fragment: F) -> F { - LoggerFragmentBuilder.buildBlock(fragment) - } - - /// Build a block from no fragments (empty block). - public static func buildBlock() -> LiteralFragment { - LoggerFragmentBuilder.buildBlock() - } - - /// Build the first fragment in a partial block. - public static func buildPartialBlock(first: F) -> F { - first - } - - /// Combine accumulated fragments with the next fragment using ``AndFragment``. - public static func buildPartialBlock( - accumulated: F1, - next: F2 - ) -> AndFragment> { - AndFragment(accumulated, next.separated(" ")) - } - - /// Handle optional fragments using an optional wrapper. - public static func buildOptional(_ fragment: F?) -> OptionalFragment { - LoggerFragmentBuilder.buildOptional(fragment) - } - - /// Build either branch for if-else statements (first branch). - public static func buildEither(first fragment: F) -> F { - LoggerFragmentBuilder.buildEither(first: fragment) - } - - /// Build either branch for if-else statements (second branch). - public static func buildEither(second fragment: F) -> F { - LoggerFragmentBuilder.buildEither(second: fragment) - } - - /// Build an array of fragments using a custom ``ArrayFragment``. - public static func buildArray(_ fragments: [F]) -> ArrayFragment { - ArrayFragment(fragments, separator: " ") - } -} diff --git a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift index dd370aa..b1d53c2 100644 --- a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift +++ b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift @@ -4,6 +4,55 @@ import Testing @Suite("LoggerFragmentBuilder Tests") struct LoggerFragmentBuilderTests { + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) + @Test("LoggerFragmentBuilder") + func loggerFragmentBuilder() throws { + let printer = TestingConsoleLoggerPrinter() + + @LoggerFragmentBuilder<1> + var fragment: some LoggerFragment { + "Test" + LabelFragment() + LevelFragment() + MessageFragment() + MetadataFragment() + SourceLocationFragment() + } + + let logger = Logger(label: "codes.vapor.console") { label in + ConsoleLogger(fragment: fragment, printer: printer, label: label) + } + + logger.info("Test message", metadata: ["key": "value"], line: 1) + + #expect(printer.testOutputQueue.first == "Test [ codes.vapor.console ] [ INFO ] Test message [key: value] (\(#fileID):1)") + } + + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) + @Test("LoggerFragmentBuilder with zero spaces") + func loggerFragmentBuilderZeroSpaces() throws { + let printer = TestingConsoleLoggerPrinter() + + @LoggerFragmentBuilder<0> + var fragment: some LoggerFragment { + "Test" + LabelFragment() + LevelFragment() + MessageFragment() + MetadataFragment() + SourceLocationFragment() + } + + let logger = Logger(label: "codes.vapor.console") { label in + ConsoleLogger(fragment: fragment, printer: printer, label: label) + } + + logger.info("Test message", metadata: ["key": "value"], line: 1) + + #expect(printer.testOutputQueue.first == "Test[ codes.vapor.console ][ INFO ]Test message[key: value](\(#fileID):1)") + } + + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Simple Fragment") func simpleFragment() throws { let printer = TestingConsoleLoggerPrinter() @@ -23,6 +72,7 @@ struct LoggerFragmentBuilderTests { #expect(printer.testOutputQueue.first == "ConsoleLogger [ codes.vapor.console ] [ INFO ] Test message") } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Conditional Fragment", arguments: [true, false]) func conditionalFragment(includeTimestamp: Bool) throws { let printer = TestingConsoleLoggerPrinter() @@ -47,6 +97,7 @@ struct LoggerFragmentBuilderTests { } } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Array Fragment") func arrayFragment() throws { let printer = TestingConsoleLoggerPrinter() @@ -67,6 +118,7 @@ struct LoggerFragmentBuilderTests { #expect(printer.testOutputQueue.first == "[PREFIX1] [ INFO ] [PREFIX2] [ INFO ] Test message") } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Empty Block") func emptyBlock() throws { let printer = TestingConsoleLoggerPrinter() @@ -81,6 +133,7 @@ struct LoggerFragmentBuilderTests { #expect(printer.testOutputQueue.first == "") } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Complex Conditional Fragment", arguments: [Logger.Level.error, .warning, .info]) func complexConditionalFragment(level: Logger.Level) throws { let printer = TestingConsoleLoggerPrinter() @@ -110,6 +163,7 @@ struct LoggerFragmentBuilderTests { } } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Default built with LoggerFragmentBuilder") func defaultFragment() throws { let loggerBuilderPrinter = TestingConsoleLoggerPrinter() @@ -136,4 +190,23 @@ struct LoggerFragmentBuilderTests { #expect(loggerBuilderPrinter.testOutputQueue[0] == defaultLoggerPrinter.testOutputQueue[0]) } + + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) + @Test("Empty separator does not consume needsSeparator") + func emptySeparator() throws { + let printer = TestingConsoleLoggerPrinter() + let logger = Logger(label: "codes.vapor.console") { label in + ConsoleLogger(printer: printer, label: label) { + "Hello" + LevelFragment().separated("") + MessageFragment().separated(" ") + } + } + + logger.info("Test message") + + // `.separated("")` should not insert any text but also should not consume the `needsSeparator` flag, + // so the next `.separated(" ")` still inserts a space. + #expect(printer.testOutputQueue.first == "Hello[ INFO ] Test message") + } }