-
Notifications
You must be signed in to change notification settings - Fork 696
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
Initial "Dotnet list package json output" implementation #4743
Conversation
@NuGet/nuget-client |
Console.WriteLine(Strings.ListPkg_AutoReferenceDescription); | ||
reportRenderer.WriteLine(Strings.ListPkg_AutoReferenceDescription); | ||
|
||
// Should log into json output ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autoReferenceFound
is for the package referenced from sdk.
It's something we didn't consider for spec, so I far I haven't seen actual example of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test a project that targets netstandard2.0. It should have an implicit package reference.
0fbca57
to
a68b183
Compare
reportRenderer.WriteLine(); | ||
reportRenderer.WriteLine(Strings.ListPkg_SourcesUsedDescription); | ||
ProjectPackagesPrintUtility.PrintSources(listPackageArgs); | ||
reportRenderer.WriteLine(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this, coupled with the fact that JsonRenderer
has a bunch of methods that are no-ops, the text renderer also has some methods that are no-ops, and the fact that there's a if (render is JsonRenderer)
below, that there isn't a good match in abstractions.
Effectively, this ListPackageCommandRunner
is written in a way that orders its work based on the console output, and then you're trying to bolt a json renderer on top of this.
Instead, I propose something similar to a web MVC architecture. A "controller" which gets the information for a single project (the model), and then the view (renderer) knows how to render one of these models. Something like:
renderer.Start(); // no-op for command line, for json it writes `{` and version property
foreach (var project in allProjects)
{
// args is set up before the loop, from the command line arguments. It tells the controller
// what properties of the model to populate, which is how it will know to be "offline only"
// for certain text output scenarios.
var model = await controller.GetPackagesAsync(project, args);
renderer.Write(project);
}
renderer.End(); // json writes errors, other info?. does console output any "wrapping up" info?
Honestly, the "model" could actually be the model for JSON output, and then the renderer is little more than a JsonSerializer.Serialize(console.out, model)
, and the only place "logic" is needed is the text renderer.
This way IReportRenderer
doesn't need a bunch of methods that are only implemented in one of the two renderers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this, coupled with the fact that
JsonRenderer
has a bunch of methods that are no-ops, the text renderer also has some methods that are no-ops, and the fact that there's aif (render is JsonRenderer)
below, that there isn't a good match in abstractions.
Removed if (render is JsonRenderer)
logic, it can be abstract away.
Effectively, this
ListPackageCommandRunner
is written in a way that orders its work based on the console output, and then you're trying to bolt a json renderer on top of this.Instead, I propose something similar to a web MVC architecture. A "controller" which gets the information for a single project (the model), and then the view (renderer) knows how to render one of these models. Something like:
renderer.Start(); // no-op for command line, for json it writes `{` and version property foreach (var project in allProjects) { // args is set up before the loop, from the command line arguments. It tells the controller // what properties of the model to populate, which is how it will know to be "offline only" // for certain text output scenarios. var model = await controller.GetPackagesAsync(project, args); renderer.Write(project); } renderer.End(); // json writes errors, other info?. does console output any "wrapping up" info?Honestly, the "model" could actually be the model for JSON output, and then the renderer is little more than a
JsonSerializer.Serialize(console.out, model)
, and the only place "logic" is needed is the text renderer.This way
IReportRenderer
doesn't need a bunch of methods that are only implemented in one of the two renderers.
Thank you for feedback.
- With this approach we couldn't print all the problems on top of json report, instead it would be written to the bottom of report. Is it ok?
Current json template is
{
"version": 1,
"parameters": "",
"problems": [
{
"project": "src/lib/MyProjectB.csproj",
"message": "The project `C:/Users/userA/repos/MainApp/src/lib/MyProjectB.csproj`` uses package.config for NuGet packages, while the command works only with package reference projects."
}
],
"projects": [
{
...
}
]
}
Instead, we would do:
{
"version": 1,
"parameters": "",
"projects": [
{
...
}
],
"problems": [
{
"project": "src/lib/MyProjectB.csproj",
"message": "The project `C:/Users/userA/repos/MainApp/src/lib/MyProjectB.csproj`` uses package.config for NuGet packages, while the command works only with package reference projects."
}
]
}
- Also making it MVC style means I need to completely rewrite the current console output logic too. I'm not sure unless I divide into phases not sure how much additional effort it would take.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this approach we couldn't print all the problems on top of json report, instead it would be written to the bottom of report. Is it ok?
Two things:
-
Section 1 of the JSON specification says:
An object is an unordered collection of zero or more name/value
pairs, where a name is a string and a value is a string, number,
boolean, null, object, or array.An array is an ordered sequence of zero or more values.
In other words, it is "invalid" to depend on specific order of properties in an object. Having said that, we really, really need version to be the very first property so that tools can implement polymorphic deserialization without buffering the entire json (for when a version 2 output eventually exists).
-
The JSON renderer's implementation of
void Write(ProjectInfo)
could be just to add the object into a list, and it doesn't actually output anything until thevoid End()
at the end of the loop.
The big picture I was trying to raise awareness over is overall readability and "software architecture". Let's avoid leaky abstractions. Don't get bogged down in the example comments that were designed to help understand the API design. How different renderers implement the APIs are implementation specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Section 1 of the JSON specification says:
An object is an unordered collection of zero or more name/value
pairs, where a name is a string and a value is a string, number,
boolean, null, object, or array.
An array is an ordered sequence of zero or more values.In other words, it is "invalid" to depend on specific order of properties in an object. Having said that, we really, really need version to be the very first property so that tools can implement polymorphic deserialization without buffering the entire json (for when a version 2 output eventually exists).
Of course, it doesn't make any difference for machine which order is objects are in json, but someone looking at the report have to scroll down all the way to find out what was the issue (for example admin looking at non-zero exit code from json output, in case there're hundreds of projects and thousands of dependencies then finding what went wrong requires writing own parse). Current structure allows to find out if there was issue at 1st glance and it's consistent what npm -ls json
doing it, I feel having errors/problems at top is more user friendly.
- The JSON renderer's implementation of
void Write(ProjectInfo)
could be just to add the object into a list, and it doesn't actually output anything until thevoid End()
at the end of the loop.
I added void Write(ReportProject reportProject)
method into IRenderer interface
and Void End()
is doing the action rendering, maybe this could be not enough to address your concern, let me know if I need to make more drastic change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but someone looking at the report have to scroll down all the way to find out what was the issue
The feature is called "machine readable". You're right that this doesn't mean we should make it difficult for humans to read. But customers are expected to use it in scripts or pipe to other commands, not to output to the console and look at manually. Those scripts they create can reply the errors to their CI logs, to the console, to whereever. Alternatively, if the customer runs the command without --format json
, we expect them to see the same errors (but in a more human friendly text output).
I feel having errors/problems at top is more user friendly.
Only if the output is piped to something like more
or less
, right? If you just run dotnet list pacakges --format json
and there are pages and pages of output, when the terminal stops scrolling, you're only going to see what's at the end, not at the beginning.
I added void Write(ReportProject reportProject) method into IRenderer interface and Void End() is doing the action rendering, maybe this could be not enough to address your concern, let me know if I need to make more drastic change.
I feel that since both the text renderer and json renderer have interface methods which they ignore, it's a code smell that it's not well designed. I get that it's quicker because you don't need to de-tangle business logic from presentation. But think about it from the point of view of someone who hasn't joined the NuGet team yet (or who doesn't review this PR). In the future they'll get some work item to work on dotnet list package
, they'll be reading the code for the first time, and trying to understand why it's calling renderer.WriteLine(someString)
followed by renderer.Write(ProjectInfo)
. Why doesn't renderer.Write(ProjectInfo
generate someString
itself? Another also, I know this isn't a performance hot path, but all those times where the business logic does renderer.WriteLine(string.Format(...))
, the runtime will need to parse the format string, create a string builder, create the string, return to caller, which then gets passed to the json renderer, just to ignore it. Not a performance hot path, but every code path is an opportunity to practise best practices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that since both the text renderer and json renderer have interface methods which they ignore, it's a code smell that it's not well designed.
I agree with this.
The abstraction is there in order to facilitate fewer code changes which isn't good design.
Part of the reason why we've always had trouble with making changes to dotnet list package has been the lack of proper abstractions. I feel like this exacerbates that.
In other words, there's an interface with 10 methods, but only 5 of those methods are actually common.
4af16f5
to
a68b183
Compare
src/NuGet.Core/NuGet.CommandLine.XPlat/ReportRenderers/JsonRenderer/JsonOutputContent.cs
Outdated
Show resolved
Hide resolved
5571189
to
3b6d90c
Compare
3b6d90c
to
c371ad0
Compare
a980649
to
8139fe1
Compare
…her dotnet command like 'dotnet nuget list source' besides current 'dotnet list package'
2422c73
to
13d3c84
Compare
|
||
namespace NuGet.CommandLine.XPlat | ||
{ | ||
internal interface IReportRenderer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Make the IReportRendered independent of exit code
classDiagram
class IReportRenderer~T~:::abstract {
TextWriter writer
WriteOutput(T reportContent)
}
IReportRenderer~T~ <|-- ConsoleWriter : implements
IReportRenderer~T~ <|-- JsonPackageRendered : implements
Maybe rename ConsoleWriter
, JsonPackageRendered
types to reflect that they implement IReportRenderer
I think exit code is independent of text output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left the same feedback as @dominoFire in a separate comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Big issue why you're having a tough time writing easy to follow code is the lack of good abstractions in this implementation.
I feel like this exacerbates that.
dotnet list package is a command that's had a lot of changes/additions. I think we should take the time to get it done correctly so that we don't have these issues in the future.
Fortunately, not of these types are public APIs, so we can do better.
IReportRenderer jsonReportRenderer; | ||
|
||
var currentlySupportedReportVersions = new List<string> { "1" }; | ||
// If customer pass unsupported version then default to latest available version and warn about unsupported version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment here is warn, but we seem to log jsonReportRenderer.WriteErrorLine.
Shouldn't this be a 1 exit code, invalid input?
Seems better than ignoring a bad parameter.
reportRenderer.WriteLine(); | ||
reportRenderer.WriteLine(Strings.ListPkg_SourcesUsedDescription); | ||
ProjectPackagesPrintUtility.PrintSources(listPackageArgs); | ||
reportRenderer.WriteLine(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that since both the text renderer and json renderer have interface methods which they ignore, it's a code smell that it's not well designed.
I agree with this.
The abstraction is there in order to facilitate fewer code changes which isn't good design.
Part of the reason why we've always had trouble with making changes to dotnet list package has been the lack of proper abstractions. I feel like this exacerbates that.
In other words, there's an interface with 10 methods, but only 5 of those methods are actually common.
{ | ||
try | ||
{ | ||
outputFormat = EnumExtensions.GetValueFromName<ReportOutputFormat>(outputFormatOption); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not use enum parse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
catch (ArgumentException) | ||
{ | ||
string currentlySupportedFormat = "console, json"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be generated by looking at all the values of the enum, not by hardcoding it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is temporary; I want feedback on main design.
} | ||
|
||
listPackageArgs.Renderer.End(); | ||
return listPackageArgs.Renderer.ExitCode(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The renderer having the exit code feels weird.
Roughly, the design should be something that returns a model and then something else that renders that.
The model should contain the failure information.
|
||
namespace NuGet.CommandLine.XPlat | ||
{ | ||
internal interface IReportRenderer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left the same feedback as @dominoFire in a separate comment.
This PR has been automatically marked as stale because it has no activity for 7 days. It will be closed if no further activity occurs within another 7 days of this comment. If it is closed, you may reopen it anytime when you're ready again, as long as you don't delete the branch. |
Bug
Fixes: NuGet/Home#7752
Regression? Last working version:
Description
Implement Dotnet List Package Machine Readable Json Output spec (spec PR), e.g.
dotnet list package --format json
will give you json output.If there any error with json generation then application'll return
non-zero
value.This is draft implementation just to check if my technical implementation design going correct direction, if anyone has concern then better to address concern going too far into wrong direction, since it's 1st json output for dotnet so always room for learn and improve. Ideally, we should be MVC style pattern but there's tight coupling between business logic and console output from previous implementation, it's very hard to make MVC style at this moment.
Please just quickly glance at PR and let me know if there are any serious concern, I know implementation is not complete there is some discrepancy what you see here and spec, I'll address them later.
dotnet list package
Here is current basic output looks like for
dotnet list package --format json
:PR Checklist
PR has a meaningful title
PR has a linked issue.
Described changes
Tests
Documentation