Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds output formatters #127

Merged
merged 2 commits into from
Aug 11, 2023
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
40 changes: 31 additions & 9 deletions .build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ public class Build : NukeBuild
var commands = new List<CommandTask>();

var queue = new Stack<string>();
var parentSymbols = new Stack<Symbol[]>();
parentSymbols.Push(builder.Command.Options
.Where(x => x is { Name: not "help" and not "version" })
.OfType<Symbol>()
.Concat(builder.Command.Arguments)
.ToArray());
foreach (var subcommand in builder.Command.Subcommands)
{
Visit(subcommand, queue);
Visit(subcommand, queue, parentSymbols);
}

var buffer = new ArrayBufferWriter<byte>();
Expand All @@ -46,23 +52,26 @@ public class Build : NukeBuild
utf8JsonWriter.WriteString("definiteArgument", command.Argument);
utf8JsonWriter.WriteStartObject("settingsClass");
utf8JsonWriter.WriteStartArray("properties");
foreach (var property in command.Command.Options)

var parentOptions = command.ParentSymbols.OfType<Option>();
foreach (var property in command.Command.Options.Concat(parentOptions))
{
utf8JsonWriter.WriteStartObject();
utf8JsonWriter.WriteString("name", EncodeName(property.Name));
utf8JsonWriter.WriteString("type", "string");
utf8JsonWriter.WriteString("format", $"--{property.Name} {{value}}");
utf8JsonWriter.WriteString("help", property.Description);
utf8JsonWriter.WriteString("help", property.Description?.Replace("\n", " "));
utf8JsonWriter.WriteEndObject();
}

foreach (var property in command.Command.Arguments)
var parentArguments = command.ParentSymbols.OfType<Argument>();
foreach (var property in command.Command.Arguments.Concat(parentArguments))
{
utf8JsonWriter.WriteStartObject();
utf8JsonWriter.WriteString("name", EncodeName(property.Name));
utf8JsonWriter.WriteString("type", "string");
utf8JsonWriter.WriteString("format", "{value}");
utf8JsonWriter.WriteString("help", property.Description);
utf8JsonWriter.WriteString("help", property.Description?.Replace("\n", " "));
utf8JsonWriter.WriteEndObject();
}

Expand Down Expand Up @@ -95,22 +104,31 @@ public class Build : NukeBuild
GenerateCode(ToolSpecfication, namespaceProvider: _ => "Confix.Nuke");
return;

void Visit(Command command, Stack<string> parents)
void Visit(Command command, Stack<string> parents, Stack<Symbol[]> symbols)
{
parents.Push(command.Name);

if (command.Subcommands.Count == 0)
{
var reversed = parents.Reverse().ToList();
var name = string.Join("", reversed.Select(Capitalize));
commands.Add(new(name, string.Join(" ", reversed), command));
commands.Add(
new(name,
string.Join(" ", reversed),
command,
symbols.SelectMany(x => x).ToArray()));
}
else
{
symbols.Push(command.Options.OfType<Symbol>()
.Concat(command.Arguments)
.ToArray());
foreach (var subcommand in command.Subcommands)
{
Visit(subcommand, parents);
Visit(subcommand, parents, symbols);
}

symbols.Pop();
}

parents.Pop();
Expand All @@ -129,4 +147,8 @@ private static string Capitalize(string name)
}
}

public record CommandTask(string Name, string Argument, Command Command);
public record CommandTask(
string Name,
string Argument,
Command Command,
IReadOnlyList<Symbol> ParentSymbols);
10 changes: 10 additions & 0 deletions src/Confix.Tool/src/Confix.Library/Common/ContextData/Context.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Confix.Tool;

public static class Context
{
public static Key<bool> DisableStatus { get; } = new("Confix.Tool.Settings.DisableStatus");

public static Key<object> Output { get; } = new("Confix.Tool.Output");

public readonly record struct Key<T>(string Id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.Runtime.CompilerServices;
using Confix.Extensions;
using Microsoft.Extensions.DependencyInjection;

namespace Confix.Tool;

public static class ContextDataCommandLineBuilderExtensions
{
private static readonly ConditionalWeakTable<CommandLineBuilder, ContextData> _contextData =
new();

public static CommandLineBuilder AddContextData(this CommandLineBuilder builder)
{
var contextData = new ContextData();
builder.AddSingleton(contextData);
_contextData.Add(builder, contextData);

return builder;
}

public static CommandLineBuilder SetContextData<T>(
this CommandLineBuilder builder,
Context.Key<T> key,
T value)
where T : notnull
{
builder.AddMiddleware((context, next) =>
{
var data = context.BindingContext.GetRequiredService<ContextData>();
data.Data.Set(key, value);

return next(context);
});

return builder;
}

public static IDictionary<string, object> GetContextData(this InvocationContext context)
{
return context.BindingContext.GetRequiredService<ContextData>().Data;
}

public static IDictionary<string, object> GetContextData(this CommandLineBuilder builder)
{
return _contextData.GetOrCreateValue(builder).Data;
}

public static void SetContextData<T>(
this InvocationContext context,
in Context.Key<T> key,
T value)
where T : notnull
{
var data = context.BindingContext.GetRequiredService<ContextData>();
data.Data.Set(key, value);
}

private class ContextData
{
public IDictionary<string, object> Data { get; } = new Dictionary<string, object>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Diagnostics.CodeAnalysis;

namespace Confix.Tool;

public static class ContextDataExtensions
{
public static T? Get<T>(this IDictionary<string, object> contextData, in Context.Key<T> key)
{
if (contextData.TryGetValue(key.Id, out var value))
{
return (T?) value;
}

return default;
}

public static void Set<T>(
this IDictionary<string, object> contextData,
in Context.Key<T> key,
T value)
where T : notnull
{
contextData[key.Id] = value;
}

public static bool TryGetValue<T>(
this IDictionary<string, object> contextData,
in Context.Key<T> key,
[NotNullWhen(true)] out T? value)
{
if (contextData.TryGetValue(key.Id, out var v))
{
value = (T) v;
return true;
}

value = default;
return false;
}

public static T GetOrAddValue<T>(
this IDictionary<string, object> contextData,
in Context.Key<T> key) where T : new()
{
if (!contextData.TryGetValue(key.Id, out var v) || v is not T value)
{
value = new T();
contextData[key.Id] = value;
}

return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ public static CommandLineBuilder UseVerbosity(this CommandLineBuilder builder)

return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.CommandLine.Invocation;

namespace Confix.Tool.Commands.Logging;

public interface IOutputFormatter
{
bool CanHandle(OutputFormat format, object value);

Task<string> FormatAsync(InvocationContext context, OutputFormat format, object value);
}

public interface IOutputFormatter<in T> : IOutputFormatter
{
bool IOutputFormatter.CanHandle(OutputFormat format, object value)
=> value is T t && CanHandle(format, t);

bool CanHandle(OutputFormat format, T value);

Task<string> IOutputFormatter.FormatAsync(
InvocationContext context,
OutputFormat format,
object value)
{
if (value is T t)
{
return FormatAsync(context, format, t);
}

throw new ArgumentException($"Expected {typeof(T).Name} but got {value.GetType().Name}");
}

Task<string> FormatAsync(InvocationContext context, OutputFormat format, T value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.CommandLine.Builder;
using Spectre.Console;

namespace Confix.Tool.Commands.Logging;

public static class OutputFormatCommandLineBuilderExtensions
{
private static Context.Key<List<IOutputFormatter>> _key =
new("Confix.Tool.Common.OutputFormatter");

public static CommandLineBuilder AddOutputFormatter<T>(this CommandLineBuilder builder)
where T : IOutputFormatter, new()
{
builder.GetOutputFormatters().Add(new T());
return builder;
}

public static CommandLineBuilder UseOutputFormat(this CommandLineBuilder builder)
{
builder.AddMiddleware(async (context, next) =>
{
var format =
context.ParseResult.GetValueForOption(FormatOption.Instance) ??
context.ParseResult.GetValueForOption(FormatOptionWithDefault.Instance);

if (format is not null)
{
context.SetContextData(Context.DisableStatus, true);

// we disable logging if the format option is specified
using (App.Log.SetVerbosity(Verbosity.Quiet))
{
await next(context);
}

var outputFormatters = builder.GetOutputFormatters();

if (builder.GetOutput() is { } output)
{
string? formattedValue = null;

foreach (var outputFormatter in outputFormatters)
{
if (!outputFormatter.CanHandle(format.Value, output))
{
continue;
}

formattedValue =
await outputFormatter.FormatAsync(context, format.Value, output);

break;
}

context.Console.Out.Write(formattedValue);
}
}
else
{
await next(context);
}
});

return builder;
}

private static List<IOutputFormatter> GetOutputFormatters(this CommandLineBuilder builder)
=> builder.GetContextData().GetOrAddValue(_key);

private static object? GetOutput(this CommandLineBuilder context)
{
return context.GetContextData().Get(Context.Output);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Confix.Tool.Common.Pipelines;

namespace Confix.Tool.Commands.Logging;

public static class OutputMiddlewareContextExtensions
{
public static void SetOutput(this IMiddlewareContext context, object? result)
{
if (result is null)
{
return;
}

context.ContextData.Set(Context.Output, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static class CommandExtensions
async Task<int> Handler(InvocationContext context)
{
// create the pipeline from the definition with the binding context
var executor = definition.BuildExecutor(context.BindingContext);
var executor = definition.BuildExecutor(context);

command.Arguments.ForEach(argument =>
{
Expand Down
14 changes: 12 additions & 2 deletions src/Confix.Tool/src/Confix.Library/Common/Pipelines/Pipeline.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.CommandLine;
using System.CommandLine.Invocation;

namespace Confix.Tool.Common.Pipelines;

Expand All @@ -9,7 +10,7 @@

protected abstract void Configure(IPipelineDescriptor builder);

protected Pipeline()

Check warning on line 13 in src/Confix.Tool/src/Confix.Library/Common/Pipelines/Pipeline.cs

View workflow job for this annotation

GitHub Actions / ci-build

Non-nullable property 'ContextData' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
{
Initialize();
}
Expand All @@ -31,9 +32,18 @@

public IReadOnlySet<Option> Options { get; private set; } = new HashSet<Option>();

public PipelineExecutor BuildExecutor(IServiceProvider services)
public PipelineExecutor BuildExecutor(InvocationContext context)
{
return new PipelineExecutor(BuildDelegate(services), services, ContextData);
var services = context.BindingContext;

// as we only use a single instance of the pipeline, we reuse the context data
var contextData = context.GetContextData();
foreach (var (key, value) in ContextData)
{
contextData[key] = value;
}

return new PipelineExecutor(BuildDelegate(services), services, contextData);
}

public async Task ExecuteAsync(IMiddlewareContext context)
Expand Down
Loading
Loading