Skip to content

Commit fb136be

Browse files
committed
feat: enhance ILoggerExtensions and add new message writers for console logging
- Updated ProgressAsync and StatusAsync methods to use ProgressContext and StatusContext respectively. - Introduced AskConsoleMessageWriter, ConfirmConsoleMessageWriter, and ProgressConsoleMessageWriter for handling specific log message types. - Added new payload classes: AskPayload, ConfirmPayload, ProgressPayload, and StatusPayload to encapsulate execution logic. - Removed obsolete ProgressStartPayload and ProgressFinishedPayload classes. - Added SpectreConsoleLogSinkExtensions for disabling log sink filters. - Updated tests for ProgressAsync and StatusAsync methods to reflect changes in method signatures and behavior.
1 parent 5b79608 commit fb136be

16 files changed

Lines changed: 215 additions & 153 deletions

src/Extensions/ILoggerExtensions.cs

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using Spectre.Console;
@@ -82,43 +83,46 @@ public static ILogger HorizontalRule(this ILogger @this, string? title = null)
8283
/// Logs a <see cref="Progress"/> to the console.
8384
/// </summary>
8485
/// <param name="this">The <see cref="ILogger"/> instance to log the progress to.</param>
85-
/// <param name="action">An action that receives the <see cref="Progress"/> instance to configure it and add tasks to it.</param>
86+
/// <param name="action">An action that receives the <see cref="ProgressContext"/> instance to configure it and add tasks to it.</param>
8687
/// <param name="cancellationToken">A cancellation token that can be used to cancel the progress operation.</param>
8788
/// <returns>A <see cref="Task"/> that represents the asynchronous progress operation.</returns>
88-
public static async Task ProgressAsync(this ILogger @this, Func<Progress, Task> action, CancellationToken cancellationToken = default)
89+
public static Task ProgressAsync(this ILogger @this, Func<ProgressContext, Task> action, CancellationToken cancellationToken = default)
8990
{
9091
ArgumentNullException.ThrowIfNull(@this);
91-
ArgumentNullException.ThrowIfNull(action);
92-
93-
ProgressStartPayload startPayload = new()
94-
{
95-
CancellationToken = cancellationToken,
96-
};
97-
98-
@this.Log(null, startPayload);
99-
100-
Progress progress = await startPayload.WaitForProgressAsync().ConfigureAwait(false);
10192

102-
await action(progress).ConfigureAwait(false);
93+
TaskCompletionSource taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
10394

104-
ProgressFinishedPayload finishedPayload = new()
95+
ProgressPayload payload = new()
10596
{
106-
CancellationToken = cancellationToken,
97+
ExecuteAsync = async console =>
98+
{
99+
try
100+
{
101+
await console.Progress().StartAsync(action).ConfigureAwait(false);
102+
103+
taskCompletionSource.SetResult();
104+
}
105+
catch (Exception exception)
106+
{
107+
taskCompletionSource.SetException(exception);
108+
}
109+
},
107110
};
108111

109-
@this.Log(null, finishedPayload);
112+
@this.Log(null, payload);
110113

111-
await finishedPayload.WaitForFinishedAsync().ConfigureAwait(false);
114+
return taskCompletionSource.Task;
112115
}
113116

114117
/// <summary>
115118
/// Logs a <see cref="Status"/> to the console.
116119
/// </summary>
117120
/// <param name="this">The <see cref="ILogger"/> instance to log the status to.</param>
121+
/// <param name="status">The status text to display.</param>
118122
/// <param name="action">An action that receives the <see cref="Status"/> instance to configure it and add tasks to it.</param>
119123
/// <param name="cancellationToken">A cancellation token that can be used to cancel the status operation.</param>
120124
/// <returns>A <see cref="Task"/> that represents the asynchronous status operation.</returns>
121-
public static async Task StatusAsync(this ILogger @this, Func<Status, Task> action, CancellationToken cancellationToken = default)
125+
public static Task StatusAsync(this ILogger @this, string status, Func<StatusContext, Task> action, CancellationToken cancellationToken = default)
122126
{
123127
ArgumentNullException.ThrowIfNull(@this);
124128
ArgumentNullException.ThrowIfNull(action);
@@ -128,19 +132,88 @@ public static async Task StatusAsync(this ILogger @this, Func<Status, Task> acti
128132
CancellationToken = cancellationToken,
129133
};
130134

131-
@this.Log(null, startPayload);
135+
TaskCompletionSource taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
132136

133-
Status status = await startPayload.WaitForStatusAsync().ConfigureAwait(false);
137+
StatusPayload statusPayload = new()
138+
{
139+
ExecuteAsync = async console =>
140+
{
141+
try
142+
{
143+
await console.Status().StartAsync(status, action).ConfigureAwait(false);
144+
145+
taskCompletionSource.SetResult();
146+
}
147+
catch (Exception exception)
148+
{
149+
taskCompletionSource.SetException(exception);
150+
}
151+
},
152+
};
134153

135-
await action(status).ConfigureAwait(false);
154+
@this.Log(null, statusPayload);
136155

137-
StatusFinishedPayload finishedPayload = new()
156+
return taskCompletionSource.Task;
157+
}
158+
159+
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to catch all exceptions to set them on the payload.")]
160+
public static Task<bool> ConfirmAsync(this ILogger @this, string prompt, bool defaultValue = true, CancellationToken cancellationToken = default)
161+
{
162+
ArgumentNullException.ThrowIfNull(@this);
163+
ArgumentNullException.ThrowIfNull(prompt);
164+
165+
TaskCompletionSource<bool> taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
166+
167+
ConfirmPayload payload = new()
138168
{
139-
CancellationToken = cancellationToken,
169+
ExecuteAsync = async console =>
170+
{
171+
try
172+
{
173+
bool result = await console.ConfirmAsync(prompt, defaultValue, cancellationToken: cancellationToken).ConfigureAwait(false);
174+
175+
taskCompletionSource.SetResult(result);
176+
}
177+
catch (Exception exception)
178+
{
179+
taskCompletionSource.SetException(exception);
180+
}
181+
},
182+
};
183+
184+
@this.Log(null, payload);
185+
186+
return taskCompletionSource.Task;
187+
}
188+
189+
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to catch all exceptions to set them on the payload.")]
190+
public static Task<T> AskAsync<T>(this ILogger @this, string prompt, CancellationToken cancellationToken = default)
191+
where T : notnull
192+
{
193+
ArgumentNullException.ThrowIfNull(@this);
194+
ArgumentNullException.ThrowIfNull(prompt);
195+
196+
TaskCompletionSource<T> taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
197+
198+
AskPayload payload = new()
199+
{
200+
ExecuteAsync = async console =>
201+
{
202+
try
203+
{
204+
T result = await console.AskAsync<T>(prompt, cancellationToken: cancellationToken).ConfigureAwait(false);
205+
206+
taskCompletionSource.TrySetResult(result);
207+
}
208+
catch (Exception exception)
209+
{
210+
taskCompletionSource.TrySetException(exception);
211+
}
212+
},
140213
};
141214

142-
@this.Log(null, finishedPayload);
215+
@this.Log(null, payload);
143216

144-
await finishedPayload.WaitForFinishedAsync().ConfigureAwait(false);
217+
return taskCompletionSource.Task;
145218
}
146219
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
3+
namespace WB.Logging.LogSinks.Console.Spectre;
4+
5+
internal static class SpectreConsoleLogSinkExtensions
6+
{
7+
internal static IDisposable Disable(this SpectreConsoleLogSink logSink)
8+
=> logSink.AddFilter(_ => false);
9+
}
10+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using WB.Logging.LogSinks.Base;
6+
7+
namespace WB.Logging.LogSinks.Console.Spectre;
8+
9+
internal sealed class AskConsoleMessageWriter(SpectreConsoleLogSink logSink) : IAsyncLogMessageWriter<AskPayload>
10+
{
11+
public async ValueTask WriteAsync(ILogMessage<AskPayload> logMessage, CancellationToken cancellationToken)
12+
{
13+
using IDisposable filter = logSink.Disable();
14+
15+
await logMessage.Payload.ExecuteAsync(logSink.Console).ConfigureAwait(false);
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
using WB.Logging.LogSinks.Base;
7+
8+
namespace WB.Logging.LogSinks.Console.Spectre;
9+
10+
internal sealed class ConfirmConsoleMessageWriter(SpectreConsoleLogSink logSink) : IAsyncLogMessageWriter<ConfirmPayload>
11+
{
12+
public async ValueTask WriteAsync(ILogMessage<ConfirmPayload> logMessage, CancellationToken cancellationToken)
13+
{
14+
using IDisposable filter = logSink.Disable();
15+
16+
await logMessage.Payload.ExecuteAsync(logSink.Console).ConfigureAwait(false);
17+
}
18+
}
Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Diagnostics.CodeAnalysis;
42
using System.Threading;
53
using System.Threading.Tasks;
6-
using Spectre.Console;
74
using WB.Logging.LogSinks.Base;
85

96
namespace WB.Logging.LogSinks.Console.Spectre;
@@ -13,38 +10,16 @@ namespace WB.Logging.LogSinks.Console.Spectre;
1310
/// render progress updates in the console.
1411
/// </summary>
1512
internal sealed class ProgressConsoleMessageWriter(SpectreConsoleLogSink logSink)
16-
: IAsyncLogMessageWriter<ProgressStartPayload>
17-
, IAsyncLogMessageWriter<ProgressFinishedPayload>
13+
: IAsyncLogMessageWriter<ProgressPayload>
1814
{
19-
private IDisposable? logSinkDisabledSubscription;
20-
2115
// ┌─────────────────────────────────────────────────────────────────────────────┐
2216
// │ Public Methods │
2317
// └─────────────────────────────────────────────────────────────────────────────┘
2418

25-
public ValueTask WriteAsync(ILogMessage<ProgressStartPayload> logMessage, CancellationToken cancellationToken)
26-
{
27-
if (logSinkDisabledSubscription is null)
28-
{
29-
logSinkDisabledSubscription = logSink.AddFilter<ProgressFinishedPayload>(lm => lm.Payload is ProgressFinishedPayload);
30-
31-
logMessage.Payload.SetProgress(logSink.Console.Progress());
32-
}
33-
34-
return ValueTask.CompletedTask;
35-
}
36-
37-
public ValueTask WriteAsync(ILogMessage<ProgressFinishedPayload> logMessage, CancellationToken cancellationToken)
19+
public async ValueTask WriteAsync(ILogMessage<ProgressPayload> logMessage, CancellationToken cancellationToken)
3820
{
39-
if (logSinkDisabledSubscription is not null)
40-
{
41-
logSinkDisabledSubscription.Dispose();
42-
logSinkDisabledSubscription = null;
21+
using IDisposable filter = logSink.Disable();
4322

44-
logMessage.Payload.SetFinished();
45-
}
46-
47-
return ValueTask.CompletedTask;
23+
await logMessage.Payload.ExecuteAsync(logSink.Console).ConfigureAwait(false);
4824
}
49-
5025
}

src/LogMessageWriters/StatusConsoleMessageWriter.cs

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,17 @@ namespace WB.Logging.LogSinks.Console.Spectre;
1212
/// A log message writer that uses Spectre.Console's status to
1313
/// render status updates in the console.
1414
/// </summary>
15-
internal sealed class StatusConsoleMessageWriter
16-
: IAsyncLogMessageWriter<StatusStartPayload>
17-
, IAsyncLogMessageWriter<StatusFinishedPayload>
15+
internal sealed class StatusConsoleMessageWriter(SpectreConsoleLogSink logSink)
16+
: IAsyncLogMessageWriter<StatusPayload>
1817
{
19-
private IDisposable? logSinkDisabledSubscription;
20-
21-
// ┌─────────────────────────────────────────────────────────────────────────────┐
22-
// │ Public Properties │
23-
// └─────────────────────────────────────────────────────────────────────────────┘
24-
25-
/// <inheritdoc/>
26-
public IAnsiConsole Writer { get; set; } = AnsiConsole.Console;
27-
28-
/// <inheritdoc/>
29-
[NotNull]
30-
public SpectreConsoleLogSink? LogSink { get; set; }
31-
3218
// ┌─────────────────────────────────────────────────────────────────────────────┐
3319
// │ Public Methods │
3420
// └─────────────────────────────────────────────────────────────────────────────┘
3521

36-
public ValueTask WriteAsync(ILogMessage<StatusStartPayload> logMessage, CancellationToken cancellationToken)
22+
public async ValueTask WriteAsync(ILogMessage<StatusPayload> logMessage, CancellationToken cancellationToken)
3723
{
38-
if (logSinkDisabledSubscription is null && LogSink is not null)
39-
{
40-
logSinkDisabledSubscription = LogSink.AddFilter<object>(lm => lm.Payload is StatusFinishedPayload);
41-
42-
logMessage.Payload.SetStatus(LogSink.Console.Status());
43-
}
44-
45-
return ValueTask.CompletedTask;
46-
}
47-
48-
public ValueTask WriteAsync(ILogMessage<StatusFinishedPayload> logMessage, CancellationToken cancellationToken)
49-
{
50-
if (logSinkDisabledSubscription is not null)
51-
{
52-
logSinkDisabledSubscription.Dispose();
53-
logSinkDisabledSubscription = null;
54-
55-
logMessage.Payload.SetFinished();
56-
}
24+
using IDisposable filter = logSink.Disable();
5725

58-
return ValueTask.CompletedTask;
26+
await logMessage.Payload.ExecuteAsync(logSink.Console).ConfigureAwait(false);
5927
}
6028
}

src/Payloads/AskPayload.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Spectre.Console;
4+
5+
namespace WB.Logging.LogSinks.Console.Spectre;
6+
7+
internal sealed class AskPayload
8+
{
9+
public required Func<IAnsiConsole, Task> ExecuteAsync { get; init; }
10+
}

src/Payloads/ConfirmPayload.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Spectre.Console;
5+
6+
namespace WB.Logging.LogSinks.Console.Spectre;
7+
8+
internal sealed class ConfirmPayload
9+
{
10+
public required Func<IAnsiConsole, Task> ExecuteAsync { get; init; }
11+
}

src/Payloads/ProgressFinishedPayload.cs

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/Payloads/ProgressPayload.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Spectre.Console;
4+
5+
namespace WB.Logging.LogSinks.Console.Spectre;
6+
7+
internal sealed class ProgressPayload
8+
{
9+
public required Func<IAnsiConsole, Task> ExecuteAsync { get; init; }
10+
}

0 commit comments

Comments
 (0)