Please read .NET Core Guidelines for more general information about coding styles, source structure, making pull requests, and more. While this project is in the early phases of development, some of the guidelines in this document do not yet apply as strongly.
Like many open-source projects, we ask that you agree to our Contributor License Agreement before we can accept your contribution. You can sign the CLA by posting a comment in your PR. The CLA bot will tell you what to comment with.
This project can be developed on any platform. To get started, follow instructions for your OS.
This project depends on .NET Core 6.0. Before working on the project, check that .NET Core prerequisites have been met.
- Prerequisites for .NET Core on Windows
- Prerequisites for .NET Core on Linux
- Prerequisites for .NET Core on macOS
This project supports JetBrains Rider, Visual Studio 2019 or later, and Visual Studio for Mac. Any version, including the free Community Edition, should be sufficient so long as you install Visual Studio support for .NET Core development.
This project also supports using Visual Studio Code. Install the C# extension and install the .NET Core CLI to get started.
When preparing to submit changes to this project, please guidelines.
Diagram source: BDD builds momentum blog post by Seb Rose
Start all of your submissions with a behavior specification. We sometimes call these acceptance tests, because they help us verify that the freshli
application behaves in an acceptable way from the point of view of a customer.
BDD is a customer-centric process. It helps ensure that we're describing the expected behavior of the freshli
application from the perspective of the human that will be using it. Additionally, by taking this approach, we're able to have a hirer degree of confidence that the application behaves the way that we expect it to.
In this project, we're using the Gherkin language to define behavior specifications of the freshli
executable. The behavior specification are run as executable programs by Cucumber and it's companion Aruba.
Ideally, the addition or change of a Gherkin .feature
file will be the first commit in a pull request. This makes it clear that a behavior-centric and customer-centric approach to developing the functionality has been taken.
For more information on working with Cucumber, Aruba, and Behavior Driven Development, please take a peek at The Cucumber Book, Second Edition by Matt Wynne and Aslak Hellesøy, with Steve Tooke. In addition to buying directly from the publisher, Pragmatic Programmers, the book is available via the O'Reilly Learning Library. Chapter 16 of the book details using Aruba to specify and verify the behavior of a command line application.
While BDD takes a customer-centric approach to specifying the application's functionality, TDD allows us to increase the level of detail and cater more towards a developer audience.
When following TDD we first write a failing unit or integration test that describes the implementation that we want to exist. We then make that test pass by writing the simplest possible thing that could possibly work. We can then clean up the implementation that we came up with the remove needless complexity or duplication. The process then repeats with another failing unit or integration test.
The TDD process forces us to design our solutions from the point of view of the code that will be calling them. In this way, we're able to imagine the code that we want to exist from the perspective of a future developer who might have to one day work with it. TDD ensures that we consider design details early in the implementation process.
It is only natural to wonder why we choose to develop software this way. Here are some of our reasons. This is not an exhaustive list, just some of the reasons that we consider particularly important.
Ensure Testability:
Testability is an important quality of any software system. However, it is easy to craft software that is difficult or impossible to test. By authoring our tests first, we ensure that we're crafting testable solutions. When waiting to test a solution after it has been created, developers often realize that the design of their solution needs to change so that they can test it effectively.
Maintain Empathy:
When building software systems, it's also important to keep your audience in mind. BDD and TDD make it very difficult to ignore the needs of your code's audience. While it is possible to keep your audience in mind when writing test automation after an implementation has been created, it is also very easy to become overly focused on the details of your solution and forget the needs of those who are going to interact with it.
Avoid False Positives:
When writing an implementation first and then writing automated tests for it, it is very possible to construct tests that always pass. It's also possible to create tests that don't fail when the functionality that they are testing breaks. Because the BDD/TDD process starts with a failing test and then adds code to make that test pass, you can have a very high degree of confidence that the test is correctly validating the code that was authored.
On this project, we have automated compliance with the team style guide by using linting tools to help us validate that the code is styled correctly. However, there is not good tooling for summarizing all of the choices that are embedded in the configuration files for the linting tools that we're using.
A starting point:
The style that we're following for C# and Ruby was adapted from other style guides. In the case of C#, we started with the coding style that's employed by the team that maintains the .NET runtime and then made adaptations as we thought appropriate. For Ruby, our starting point is the Ruby Community Style as implemented by the Rubocop linting tool. We've attempted to document where we deviate from those style guides below.
Always include brackets for block structures
In C#, it's possible to omit the opening and closing brackets, {
and }
respectively, from some control flow statements when they contain a single line. This is possible for if
, while
, and others. On this project, we're always specifying the opening and closes brackets, even when the language permits omitting them.
Avoid this:
if (available)
Purchase();
Do this instead:
if (available)
{
Purchase();
}
Embrace the use of the var
and new()
C# is a language with a strict type system. The compiler and the runtime both enforce type checking. Older versions of the C# language required specifying these types in many different locations, even when the compiler already had enough information to determine the correct type that could be used.
The var
keyword was introduced to allow programmers to omit the when declaring a variable. This keyword is only permitted by the compiler when it has enough information to figure out the variable's type.
Similarly, the new()
keyword function call was included in the C# language to allow programmers to omit a type's name when invoking its constructor. Again, this syntax is only permitted when the compiler is able to determine the correct constructor to use.
On this project, we are embracing these additions to the C# language by using them everywhere that is permitted.
For example, given the following class:
class Example
{
public string Message { get; }
public const string DefaultMessage => "Hello";
public Example(string message)
{
Message = message;
}
}
Avoid this:
string message = Example.DefaultMessage;
Example example = new Example(message);
Do this instead:
var message = Example.DefaultMessage;
Example example = new(message);
This is also acceptable:
var message = Example.DefaultMessage;
var example = new Example(message);
While some feel that this approach makes it harder to determine the type that's being used, we feel that this objection is easily overcome by employing an editor that annotates variables with their types. In JetBrains Rider, Visual Studio, and Visual Studio Code (with the Omnisharp Extension installed), you can use the mouse to hover over a variable name to see it's type. JetBrains Rider also defaults to adding inline annotations with type information to the code.
The Ruby Style Guide includes rules for how conditional assignments should be used and indented.
On this project we are not indenting conditional assignments, and we are not always utilizing conditional assignment when all branches of a control flow structure assign to the same variable.
Avoid this:
result = if some_cond
calc_something
else
calc_something_else
end
Do this instead:
result = if some_cond
calc_something
else
calc_something_else
end
This is also acceptable:
if some_cond
result = calc_something
else
result = calc_something_else
end
Sharing work early and often helps with team collaboration. It allows us to see what each other are working on, and it creates a sense of the amount of work that is in progress. A great time to open a pull request is after you've committed an addition or a change to the Gherkin-based behavior specifications. Feel free to solicit feedback from your teammates before you think you are "done" with your changes. Push commits often as you add commits throughout the day.
Pull requests are often merged into the main
branch in a different order than they were opened. Many pull requests are also going to require addition commits are added before they are accepted. For this reason, please keep pull requests isolated from each other. When creating a branch that's going to be used for a pull request, please make sure that your local copy of the main
branch is up-to-date with the origin
version of that branch (hosted on GitHub) and create the pull request branch off of main
. This can be done with the following commands (or their equivalents):
# make sure that you're on the `main` branch
git checkout main
# make sure that you've got the last changes from `origin`/GitHub
# the `--rebase` option instructs Git to rebase an changes that you may have made directly to the `main` branch
git pull --rebase
# create the branch for your pull request
git checkout -b implement-cool-new-feature
The changes that are included in each commit should do one thing. If you are tempted to use the word "and" to describe your changes, then they should be added as multiple commits.
Many Git clients allow you to select individual lines that will be included in the next commit that you commit. The GitHub Desktop client is one such application, and it can be used to assist with this purpose. If you see a set of changes that belong in a separate commit, you can make sure that those lines are not included by deselecting them before creating the commit.
Each commit message applies a change to the codebase. To make this clear, write your commit message so that it describes what the commit will do when it is applied.
Avoid messages like this:
Added testing steps to the README.md file
Write messages like this instead:
Adds testing steps to the README.md file
Formatting rules
Commit messages are structured similar to email messages. The first line of the commit message is the "subject". (Some Git clients even separate this part of the message into its own text box to make this distinction clear.) The rest of the commit message is used for the message body.
The subject should be no more than 50 characters in length. It is sometimes very challenging to comply with this limit, so it is not strictly enforced on this project. However, please try to stay within this boundary.
The lines of the body part of the commit message should be no longer than 72 characters. Again, this is sometimes challenging so it is not strictly enforced on this project. Lines longer than 72 characters should be kept to a minimum.
Provide Context
If you think someone might look at this commit and ask themselves, "Why was it done this way?", then you should include a commit message body that describes your reasoning for making the choices that you made.
Rebase is a very powerful, and at times a very helpful, feature that's provided by the Git. It does have its drawbacks. One of these is that it rewrites history and makes it appear that was performed at a different pace than it was originally authored.
Rebase is often used to update the contents of a branch so that it contains the changes from the main
branch. Instead of relying on rebasing to accomplish this, on this project, we prefer performing merges to accomplish the same goal.
Rebase can also be used to combine multiple commits into a single commit or to split a single commit into multiple commits. If you feel the need to do this for a branch that has been pushed to origin
/GitHub or for a pull request that is already open, then please consider creating a new branch with the alternative set of commits.
There is an important advantage of creating a separate branch and pull request for these kinds of refactoring of the commit history. It makes it possible to compare the original branch with the one that has the refactored/alternative timeline. A diff can then be used to compare the branches to ensure that they still result in the same set of changes.
Consistent with this guideline, performing a "force push" to origin
/GitHub should be extremely rare. If a force push turns out to not be rare, then the project may decide to configure GitHub to prohibit a force push from being done on any branch.
No single person "owns" the code in this project. This applies to the project as a whole and to a particular feature, command, sub-system, commit, or pull request. Commits are authored by one or more people, and pull requests are opened by a single person. However, these authoring and opening activities should not be seen as conveying ownership over the related changes.
The consequence of this mindset is that any member of the team is free to propose any change to any part of the codebase and to open a pull request that includes those changes. The changes will be discussed by the team, and then applied to the main
branch if approved.
Package | Version | Description |
---|---|---|
System.CommandLine |
Command line parser, model binding, invocation, shell completions | |
System.CommandLine.Hosting |
support for using System.CommandLine with Microsoft.Extensions.Hosting | |
Corgibytes.Freshli.Lib |
Core library for collecting historical metrics about a project's dependencies | |
YamlDotNet |
A .NET library for YAML. YamlDotNet provides low level parsing and emitting of YAML as well as a high level object model similar to XmlDocument. | |
Newtonsoft.Json |
Json.NET is a popular high-performance JSON framework for .NET | |
NamedServices.Microsoft.Extensions.DependencyInjection |
Named Services for Microsoft.Extensions.DependencyInjection |
- Freshli CLI Commands are implemented using the Command Line Api library. All the commands are located under the Commands folder and are needed for the library to know how to parse user input and transform it into CommandOptions. This type of entity is where you configure the structure of your command.
- Freshli CLI Command Options represents all the options allowed for a particular Command. The input will be transformed into this object and send to the command runner. All the commands options for all the different commands are located inside the CommandOptions folder.
- Freshli CLI Command Runners are responsible for receiving a Command Options and execute the Run Command logic. It is used by the Command classes to delegate it's execution.
So far, the following commands have been implemented:
- scan - Collects historical metrics about a project's dependencies - Implemented in Commands/ScanCommand.cs, CommandOptions/ScanCommandOptions.cs and CommandRunners/ScanCommandRunner.cs
- cache - Manages the cache databased used by the other commands - Implemented in Commands/CacheCommand.cs, CommandOptions/CacheCommandOptions.cs and CommandRunners/CacheCommandRunner.cs
Follow below steps if you want to contribute with a new command:
- Add a new class called YourNewCommandNameCommandOptions into the CommandOptions folder and inherit from the base class CommandOptions. You do not need to implement anything to allow the cache-dir option, as this is inherited from the base class.
Example: CustomCommandOptions
public class CustomCommandOptions : CommandOptions
{
public string YourArgument { get ; set; }
public string YourOption { get ; set; }
// Define all the arguments and options as Properties in this class.
}
-
Add a new class called YourNewCommandNameCommand into the Commands folder and inherit it from either the generic base class RunnableCommand<> (for commands that be executed) or the base class Command (for commands that only store subcommands). You do not need to implement anything to allow the cache-dir option, as this is added globally.
Example: CustomCommand
public class CustomCommand : RunnableCommand<CustomCommandOptions>
{
public CustomCommand() : base("custom", "Custom command description")
{
// add your arguments and/or options definitioins here. For detailed information
// you have to reference the command-line-api library documentation. Below are just
// examples. See the Commands/ScanCommand.cs for an example or go to the Command Line Api (https://github.com/dotnet/command-line-api) repository for detailed information.
Option<string> yourOption= new(new[] { "--alias1", "alias2" }, description: "Option Description");
AddOption(yourOption);
Argument<string> yourArgument = new("yourargumentname", "Argument Description")
AddArgument(yourArgument);
}
}
Typically you will not need to implement the Run() method for your CustomCommand class, as this is provided by RunnableCommand<>. However, if you want to augment this behavior apart from the CommandRunner (see step 3), you can override that method:
protected override int Run(IHost host, InvocationContext context, CustomCommandOptions options)
{
// your custom logic here
return base.Run(host, context, options);
}
- Add a new class called YourNewCommandNameCommandRunner into the CommandRunners folder and inherit it from CommandRunner. Implement the Run method.
Example: CustomCommandRunners
public class CustomCommandRunner : CommandRunner<CustomCommandOptions>
{
public ScanCommandRunner(IServiceProvider serviceProvider, Runner runner): base(serviceProvider,runner)
{
}
public override int Run(CustomCommandOptions options)
{
// Implement the Run logic
}
}
- Configure your dependencies in the IoC Container. Go to the IoC folder, open the FreshliServiceBuilder.cs class and create a new method called
public void RegisterCustomCommand()
{
Services.AddScoped<ICommandRunner<CustomCommandOptions>, CustomCommandRunner>();
Services.AddOptions<YourNewCommandNameCommandOptions>().BindCommandLine();
// Add additional services as needed
}
Update the FreshliServiceBuilder Register method in order to add the invocation to this new method.
-
Go to Program.cs and add your new Command to the list at the top of the CreateCommandLineBuilder method. This will allow the main program to identify you added a new command.
-
Go to Corgibytes.Freshli.Cli.Test and add unit tests.
-
Open a Pull Request with your changes
The scan command has a --format
option, which allows specifying a formatter to use for the output of that command.
A serialization formatter is responsible for encoding objects into a particular format. The formatted output will be sent to all the selected output strategies
Available formatters are json, yaml, and csv. These formatters can be found under the Formatters folder as follows.
- CsvOutputFormatter - Encodes cli response Csv into csv format.
- JsonOutputFormatter - Encodes cli response into Json format.
- YamlOutputFormatter - Encodes cli response into Yaml format.
If you want to contribute with a formatter, you have to follow below instructions:
-
Add the new format type into the FormatType enum. Example: Custom
-
Add a new class called YourNewFormatOutputFormatter into the Formatters folder and inherit it from OutputFormatter. Implement required methods Example: CustomOutputFormatter
public class CustomOutputFormatter : OutputFormatter
{
public override FormatType Type => FormatType.Custom;
protected override string Build<T>(T entity)
{
// Implement object serialization
}
protected override string Build<T>(IList<T> entities)
{
// Implement object list serialization
}
}
- Register your new formatter class in the IoC Container. Open the FreshliServiceBuilder.cs file, search for the RegisterBaseCommand method and add a new registration line for your formatter as follows
Services.AddNamedScoped<IOutputFormatter,CustomOutputFormatter>(FormatType.Custom);
-
In order to test your new formatter, build the solution and run a command (for example the scan command), and specify your new format as input for the format option. Example:
```bash freshli scan repository-path -f custom ```
-
Go to Corgibytes.Freshli.Cli.Test and add unit tests.
-
Open a Pull Request with your changes
The scan command has an --output
option, which allows specifying one or more output strategies to use for the output of that command.
An Output Strategy is responsible for sending the serialized response of a command to a configured output. The formatted output will be sent to all the selected output strategies
Available outputs are console, file. These formatters can be found under the OutputStrategies folder as follows.
- ConsoleOutputStrategy - Sends serialized data by a formatter to the standard output.
- FileOutputStrategy - Sends serialized data by a formatter to a file.
If you want to contribute with a new output strategy, you have to follow below instructions:
-
Add the new format type into the OutputStrategyType enum. Example: Custom
-
Add a new class called YourNewOutputStrategy into the OutputStrategies folder and implement the IOutputStrategy interface. Example: CustomOutputStrategy
public class CustomOutputStrategy : IOutputStrategy
{
public OutputStrategyType Type => OutputStrategyType.Custom;
public virtual void Send(IList<MetricsResult> results, IOutputFormatter formatter, CustomCommandOptions options)
{
// Implement your logic here.
}
}
- Register your new output strategy class in the IoC Container. Open the FreshliServiceBuilder.cs file, search for the RegisterBaseCommand method and add a new registration line for your strategy as follows
Services.AddNamedScoped<IOutputStrategy, CustomOutputStrategy>(OutputStrategyType.Custom);
-
In order to test your new output strategy, build the solution and run a command (for example the scan command), and specify your new format as input for the output option. Example:
```bash freshli scan repository-path -o custom ```
-
Go to Corgibytes.Freshli.Cli.Test and add unit tests.
-
Open a Pull Request with your changes