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

Adding support for json output #191

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ A simple measure of dependency freshness

`-r`, `--recursive`: search recursively for all compatible files, even if one is found in a directory passed as an argument

`-o`, `--output`: sets how the output is displayed, valid options are `table` (default) or `json`. If `json` is selected, `--quiet` minifies the outputted json.

#### Limits:

`-l`, `--limit`: exits with error code if total libyears behind is greater than this value
Expand Down
68 changes: 10 additions & 58 deletions src/LibYear/App.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using LibYear.Core;
using LibYear.Output;
using LibYear.Output.Json;
using LibYear.Output.Table;
using Spectre.Console;

namespace LibYear;
Expand Down Expand Up @@ -27,7 +30,8 @@ public async Task<int> Run(Settings settings)
}

SteveDesmond-ca marked this conversation as resolved.
Show resolved Hide resolved
var result = await _checker.GetPackages(projects);
DisplayAllResultsTables(result, settings.QuietMode);
var output = GetOutputMethod(settings);
output.DisplayAllResults(result, settings.QuietMode);

if (settings.Update)
{
Expand All @@ -44,63 +48,11 @@ public async Task<int> Run(Settings settings)
: 0;
}

private void DisplayAllResultsTables(SolutionResult allResults, bool quietMode)
{
if (!allResults.Details.Any())
return;

int MaxLength(Func<Result, int> field) => allResults.Details.Max(results => results.Details.Any() ? results.Details.Max(field) : 0);
var namePad = Math.Max("Package".Length, MaxLength(r => r.Name.Length));
var installedPad = Math.Max("Installed".Length, MaxLength(r => r.Installed?.Version.ToString().Length ?? 0));
var latestPad = Math.Max("Latest".Length, MaxLength(r => r.Latest?.Version.ToString().Length ?? 0));

var width = allResults.Details.Max(r => r.ProjectFile.FileName.Length);
foreach (var results in allResults.Details)
GetResultsTable(results, width, namePad, installedPad, latestPad, quietMode);

if (allResults.Details.Count > 1)
{
_console.WriteLine($"Total is {allResults.YearsBehind:F1} libyears behind");
}
}

private void GetResultsTable(ProjectResult results, int titlePad, int namePad, int installedPad, int latestPad, bool quietMode)
{
if (results.Details.Count == 0)
return;

var width = Math.Max(titlePad + 2, namePad + installedPad + latestPad + 48) + 2;
var table = new Table
private IOutput GetOutputMethod(Settings settings) =>
settings.Output switch
{
Title = new TableTitle($" {results.ProjectFile.FileName}".PadRight(width)),
Caption = new TableTitle(($" Project is {results.YearsBehind:F1} libyears behind").PadRight(width)),
Width = width
OutputOption.Table => new TableOutput(_console),
OutputOption.Json => new JsonOutput(_console),
_ => throw new NotImplementedException()
};
table.AddColumn(new TableColumn("Package").Width(namePad));
table.AddColumn(new TableColumn("Installed").Width(installedPad));
table.AddColumn(new TableColumn("Released"));
table.AddColumn(new TableColumn("Latest").Width(latestPad));
table.AddColumn(new TableColumn("Released"));
table.AddColumn(new TableColumn("Age (y)"));

foreach (var result in results.Details.Where(r => !quietMode || r.YearsBehind > 0))
{
table.AddRow(
result.Name,
result.Installed?.Version.ToString() ?? string.Empty,
result.Installed?.Date.ToString("yyyy-MM-dd") ?? string.Empty,
result.Latest?.Version.ToString() ?? string.Empty,
result.Latest?.Date.ToString("yyyy-MM-dd") ?? string.Empty,
result.YearsBehind.ToString("F1")
);
}

if (quietMode && Math.Abs(results.YearsBehind) < double.Epsilon)
{
table.ShowHeaders = false;
}

_console.Write(table);
_console.WriteLine();
}
}
2 changes: 1 addition & 1 deletion src/LibYear/LibYear.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="Spectre.Console.Cli" Version="0.48.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<ProjectReference Include="../LibYear.Core/LibYear.Core.csproj" />
<None Include="../../README.md" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="." />
</ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/LibYear/Output/IOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using LibYear.Core;

namespace LibYear.Output;

public interface IOutput
{
public void DisplayAllResults(SolutionResult allResults, bool quietMode);
}
22 changes: 22 additions & 0 deletions src/LibYear/Output/Json/DateTimeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace LibYear.Output.Json;

internal sealed class DateTimeConverter : JsonConverter<DateTime>
SteveDesmond-ca marked this conversation as resolved.
Show resolved Hide resolved
{
public override DateTime Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) =>
DateTime.ParseExact(reader.GetString()!,
"yyyy-MM-dd", CultureInfo.InvariantCulture);

public override void Write(
Utf8JsonWriter writer,
DateTime dateTimeValue,
JsonSerializerOptions options) =>
writer.WriteStringValue(dateTimeValue.ToString(
"yyyy-MM-dd", CultureInfo.InvariantCulture));
}
17 changes: 17 additions & 0 deletions src/LibYear/Output/Json/DisplayVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
using LibYear.Core;

namespace LibYear.Output.Json;

internal sealed record DisplayVersion
{
[JsonPropertyName("versionNumber")]
public string VersionNumber { get; init; } = string.Empty;
[JsonPropertyName("releaseDate")]
public DateTime ReleaseDate { get; init; }
public DisplayVersion(Release release)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use the underlying Release here? Might require a JsonConverter<PackageVersion> that returns its ToString()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to put full release there, but given everything is currently public, everything would get included
That would currently look like

{
  "YearsBehind": 0,
  "DaysBehind": 0,
  "Projects": [
    {
      "Project": "test project 1",
      "YearsBehind": 0,
      "Packages": [
        {
          "PackageName": "test1",
          "CurrentVersion": {
            "versionNumber": "1.2.3",
            "releaseDate": "2024-05-30",
            "Release": {
              "Version": {
                "WildcardType": 0,
                "Version": "1.2.3.0",
                "IsLegacyVersion": false,
                "Revision": 0,
                "IsSemVer2": false,
                "OriginalVersion": null,
                "Major": 1,
                "Minor": 2,
                "Patch": 3,
                "ReleaseLabels": [],
                "Release": "",
                "IsPrerelease": false,
                "HasMetadata": false,
                "Metadata": null
              },
              "Date": "2024-05-30",
              "IsPublished": true
            }
          }
        }
      ]
    }
  ]
}

Is there anything that would to be removed/hidden/etc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, should have been clearer here. Can we just use more of the existing model objects/classes in the JSON above?

  • Packages becomes an array of Result
  • Release (of which Result has 2, Installed and Latest) contains a PackageVersion
  • Could that PackageVersion use a JsonConverter<PackageVersion> that returns just the ToString() interpretation of the version? (I may not have a proper understanding of JsonConverter<T>)

So the JSON from above would look like:

{
  "YearsBehind": 0,
  "Projects": [
    {
      "Project": "test project 1",
      "YearsBehind": 0,
      "Packages": [
        {
          "Name": "test1",
          "Installed": {
            "Version": "1.2.3",
            "Date": "2024-05-30",
            "IsPublished": true
          },
          "Latest": {
            "Version": "1.2.3",
            "Date": "2024-05-30",
            "IsPublished": true
          }
        }
      ]
    }
  ]
}

{
VersionNumber = release.Version.ToString();
ReleaseDate = release.Date;
}
}
18 changes: 18 additions & 0 deletions src/LibYear/Output/Json/DoubleFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace LibYear.Output.Json;

internal sealed class DoubleFormatter : JsonConverter<double>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some tests for this bad boy as well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also name it DoubleConverter to match the convention of the base class and DateTimeConverter?

{
public override double Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) => reader.GetDouble();

public override void Write(
Utf8JsonWriter writer,
double value,
JsonSerializerOptions options) =>
writer.WriteNumberValue(Math.Round(value, 1));
}
39 changes: 39 additions & 0 deletions src/LibYear/Output/Json/JsonOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Text.Json;
using LibYear.Core;
using Spectre.Console;

namespace LibYear.Output.Json;

public sealed class JsonOutput : IOutput
{
private readonly IAnsiConsole _console;

public JsonOutput(IAnsiConsole console)
{
_console = console;
}

public void DisplayAllResults(SolutionResult allResults, bool quietMode)
{
if (allResults.Details.Count == 0)
return;
var output = FormatOutput(allResults, quietMode);
_console.WriteLine(output);
}

private static string FormatOutput(SolutionResult allResults, bool quietMode)
{
var model = new ResultOutput(allResults);
var serializerOptions = new JsonSerializerOptions
{
Converters =
{
new DoubleFormatter(),
new DateTimeConverter()
},
WriteIndented = !quietMode
};
var output = JsonSerializer.Serialize(model, serializerOptions);
return output;
}
}
20 changes: 20 additions & 0 deletions src/LibYear/Output/Json/PackageResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
using LibYear.Core;

namespace LibYear.Output.Json;

internal sealed record PackageResult
{
public string PackageName { get; set; } = string.Empty;
public DisplayVersion? CurrentVersion { get; init; }
public DisplayVersion? LatestVersion { get; init; }
public double YearsBehind { get; init; }

public PackageResult(Result result)
{
PackageName = result.Name;
YearsBehind = result.YearsBehind;
CurrentVersion = result.Installed is null ? null : new DisplayVersion(result.Installed);
LatestVersion = result.Latest is null ? null : new DisplayVersion(result.Latest);
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we invert these ternaries (... is not null) to keep them in line with the others?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this one might be moot now if the whole class disappears)

}
}
17 changes: 17 additions & 0 deletions src/LibYear/Output/Json/ProjectFormatResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using LibYear.Core;

namespace LibYear.Output.Json;

internal sealed record ProjectFormatResult
{
public string Project { get; init; } = string.Empty;
public double YearsBehind { get; init; }
public IReadOnlyCollection<PackageResult> Packages { get; init; } = [];

public ProjectFormatResult(ProjectResult projectResult)
{
Project = projectResult.ProjectFile.FileName;
YearsBehind = projectResult.YearsBehind;
Packages = projectResult.Details.Select(result => new PackageResult(result)).ToArray();
}
}
22 changes: 22 additions & 0 deletions src/LibYear/Output/Json/ResultOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
using LibYear.Core;

namespace LibYear.Output.Json;

internal sealed record ResultOutput
{
public double YearsBehind { get; init; }
public double DaysBehind { get; init; }
public IReadOnlyCollection<ProjectFormatResult> Projects { get; set; } = [];

public ResultOutput()
{
}

public ResultOutput(SolutionResult solutionResult)
{
YearsBehind = solutionResult.YearsBehind;
DaysBehind = solutionResult.DaysBehind;
Projects = solutionResult.Details?.Select(project => new ProjectFormatResult(project)).ToArray() ?? Array.Empty<ProjectFormatResult>();
}
}
7 changes: 7 additions & 0 deletions src/LibYear/Output/OutputOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace LibYear.Output;

public enum OutputOption
{
Table,
Json
}
73 changes: 73 additions & 0 deletions src/LibYear/Output/Table/TableOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using LibYear.Core;
using Spectre.Console;

namespace LibYear.Output.Table;

public sealed class TableOutput : IOutput
{
private readonly IAnsiConsole _console;

public TableOutput(IAnsiConsole console)
{
_console = console;
}

public void DisplayAllResults(SolutionResult allResults, bool quietMode)
{
if (!allResults.Details.Any())
return;

int MaxLength(Func<Result, int> field) => allResults.Details.Max(results => results.Details.Any() ? results.Details.Max(field) : 0);
var namePad = Math.Max("Package".Length, MaxLength(r => r.Name.Length));
var installedPad = Math.Max("Installed".Length, MaxLength(r => r.Installed?.Version.ToString().Length ?? 0));
var latestPad = Math.Max("Latest".Length, MaxLength(r => r.Latest?.Version.ToString().Length ?? 0));
var width = allResults.Details.Max(r => r.ProjectFile.FileName.Length);
foreach (var results in allResults.Details)
GetResultsTable(results, width, namePad, installedPad, latestPad, quietMode);

if (allResults.Details.Count > 1)
{
_console.WriteLine($"Total is {allResults.YearsBehind:F1} libyears behind");
}
}

private void GetResultsTable(ProjectResult results, int titlePad, int namePad, int installedPad, int latestPad, bool quietMode)
{
if (results.Details.Count == 0)
return;

var width = Math.Max(titlePad + 2, namePad + installedPad + latestPad + 48) + 2;
var table = new Spectre.Console.Table
{
Title = new TableTitle($" {results.ProjectFile.FileName}".PadRight(width)),
Caption = new TableTitle(($" Project is {results.YearsBehind:F1} libyears behind").PadRight(width)),
Width = width
};
table.AddColumn(new TableColumn("Package").Width(namePad));
table.AddColumn(new TableColumn("Installed").Width(installedPad));
table.AddColumn(new TableColumn("Released"));
table.AddColumn(new TableColumn("Latest").Width(latestPad));
table.AddColumn(new TableColumn("Released"));
table.AddColumn(new TableColumn("Age (y)"));

foreach (var result in results.Details.Where(r => !quietMode || r.YearsBehind > 0))
{
table.AddRow(
result.Name,
result.Installed?.Version.ToString() ?? string.Empty,
result.Installed?.Date.ToString("yyyy-MM-dd") ?? string.Empty,
result.Latest?.Version.ToString() ?? string.Empty,
result.Latest?.Date.ToString("yyyy-MM-dd") ?? string.Empty,
result.YearsBehind.ToString("F1")
);
}

if (quietMode && Math.Abs(results.YearsBehind) < double.Epsilon)
{
table.ShowHeaders = false;
}

_console.Write(table);
_console.WriteLine();
}
}
7 changes: 6 additions & 1 deletion src/LibYear/Settings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using LibYear.Output;
using Spectre.Console.Cli;

namespace LibYear;
Expand Down Expand Up @@ -32,4 +33,8 @@ public class Settings : CommandSettings
[CommandOption("-r|--recursive")]
[Description("search recursively for all compatible files, even if one is found in a directory passed as an argument")]
public bool Recursive { get; set; }
}

[CommandOption("-o|--output")]
[Description("output format (text or json)")]
SteveDesmond-ca marked this conversation as resolved.
Show resolved Hide resolved
public OutputOption Output { get; set; } = OutputOption.Table;
}
4 changes: 2 additions & 2 deletions test/LibYear.Core.Tests/LibYear.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NuGet.Protocol" Version="6.9.1" />
<PackageReference Include="System.IO.Abstractions" Version="21.0.2" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0" />
<ProjectReference Include="../../src/LibYear.Core/LibYear.Core.csproj" />

<None Include="FileTypes/packages.config" CopyToOutputDirectory="Always" />
Expand Down
Loading