feat: initial commit
This commit is contained in:
commit
ad0525be04
36 changed files with 15711 additions and 0 deletions
122
.editorconfig
Normal file
122
.editorconfig
Normal file
|
@ -0,0 +1,122 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers=false
|
||||
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
|
||||
csharp_space_after_cast=false
|
||||
csharp_style_var_elsewhere=false:hint
|
||||
csharp_style_var_for_built_in_types=false:hint
|
||||
csharp_style_var_when_type_is_apparent=true:hint
|
||||
csharp_preserve_single_line_statements=false
|
||||
csharp_preserve_single_line_blocks=true
|
||||
dotnet_style_predefined_type_for_locals_parameters_members=true:hint
|
||||
dotnet_style_predefined_type_for_member_access=true:hint
|
||||
dotnet_style_qualification_for_event=true:hint
|
||||
dotnet_style_qualification_for_field=true:hint
|
||||
dotnet_style_qualification_for_method=true:hint
|
||||
dotnet_style_qualification_for_property=true:hint
|
||||
dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint
|
||||
|
||||
# ReSharper properties
|
||||
resharper_alignment_tab_fill_style=optimal_fill
|
||||
resharper_apply_on_completion=true
|
||||
resharper_blank_lines_after_control_transfer_statements=1
|
||||
resharper_blank_lines_around_single_line_auto_property=1
|
||||
resharper_blank_lines_around_single_line_property=1
|
||||
resharper_blank_lines_before_single_line_comment=1
|
||||
resharper_blank_lines_between_using_groups=1
|
||||
resharper_braces_for_for=required
|
||||
resharper_braces_for_foreach=required
|
||||
resharper_braces_for_ifelse=required
|
||||
resharper_braces_for_while=required
|
||||
resharper_can_use_global_alias=false
|
||||
resharper_csharp_blank_lines_around_single_line_field=1
|
||||
resharper_csharp_blank_lines_around_single_line_invocable=1
|
||||
resharper_csharp_indent_style=tab
|
||||
resharper_csharp_insert_final_newline=true
|
||||
resharper_csharp_keep_blank_lines_in_code=1
|
||||
resharper_csharp_keep_blank_lines_in_declarations=1
|
||||
resharper_csharp_new_line_before_while=true
|
||||
resharper_csharp_use_indent_from_vs=false
|
||||
resharper_csharp_wrap_arguments_style=chop_if_long
|
||||
resharper_csharp_wrap_extends_list_style=chop_if_long
|
||||
resharper_csharp_wrap_parameters_style=chop_if_long
|
||||
resharper_css_insert_final_newline=false
|
||||
resharper_enforce_line_ending_style=true
|
||||
resharper_html_insert_final_newline=false
|
||||
resharper_indent_nested_fixed_stmt=true
|
||||
resharper_js_indent_style=tab
|
||||
resharper_js_insert_final_newline=true
|
||||
resharper_js_keep_blank_lines_in_code=1
|
||||
resharper_js_stick_comment=false
|
||||
resharper_js_use_indent_from_vs=false
|
||||
resharper_js_wrap_before_binary_opsign=true
|
||||
resharper_js_wrap_chained_method_calls=chop_if_long
|
||||
resharper_keep_blank_lines_between_declarations=1
|
||||
resharper_min_blank_lines_after_imports=1
|
||||
resharper_place_attribute_on_same_line=False
|
||||
resharper_place_constructor_initializer_on_same_line=false
|
||||
resharper_place_type_constraints_on_same_line=false
|
||||
resharper_protobuf_insert_final_newline=false
|
||||
resharper_qualified_using_at_nested_scope=true
|
||||
resharper_resx_insert_final_newline=false
|
||||
resharper_space_within_single_line_array_initializer_braces=true
|
||||
resharper_use_indents_from_main_language_in_file=false
|
||||
resharper_vb_insert_final_newline=false
|
||||
resharper_wrap_after_declaration_lpar=true
|
||||
resharper_wrap_after_invocation_lpar=true
|
||||
resharper_wrap_before_extends_colon=true
|
||||
resharper_wrap_before_first_type_parameter_constraint=true
|
||||
resharper_wrap_before_type_parameter_langle=true
|
||||
resharper_xmldoc_indent_child_elements=ZeroIndent
|
||||
resharper_xmldoc_indent_text=ZeroIndent
|
||||
resharper_xmldoc_insert_final_newline=false
|
||||
resharper_xml_insert_final_newline=false
|
||||
|
||||
# ReSharper inspection severities
|
||||
resharper_check_namespace_highlighting=none
|
||||
resharper_convert_to_auto_property_highlighting=none
|
||||
resharper_localizable_element_highlighting=none
|
||||
resharper_redundant_comma_in_attribute_list_highlighting=none
|
||||
resharper_redundant_comma_in_enum_declaration_highlighting=none
|
||||
resharper_redundant_comma_in_initializer_highlighting=none
|
||||
resharper_string_compare_to_is_culture_specific_highlighting=none
|
||||
resharper_string_index_of_is_culture_specific_1_highlighting=none
|
||||
resharper_use_null_propagation_highlighting=none
|
||||
resharper_use_object_or_collection_initializer_highlighting=hint
|
||||
resharper_use_string_interpolation_highlighting=hint
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=2
|
||||
|
||||
[*.{cs,js,json,jsx,proto,resjson,ts,tsx}]
|
||||
indent_style=space
|
||||
indent_size=space
|
||||
tab_width=4
|
||||
|
||||
[*.{asax,ascx,aspx,cshtml,css,htm,html,master,razor,skin,vb,xaml,xamlx,xoml}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
tab_width=4
|
||||
|
||||
[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=2
|
||||
|
||||
[*.proto]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=2
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use asdf
|
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
launchSettings.json
|
||||
|
||||
*~
|
||||
*.user
|
||||
Directory.Build.props
|
||||
|
||||
obj/
|
||||
[Bb]in/
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
_ReSharper.Caches/
|
||||
node_modules/
|
51
.gitlab-ci.yml
Normal file
51
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,51 @@
|
|||
stages:
|
||||
- build
|
||||
|
||||
default:
|
||||
before_script:
|
||||
- curl -sL https://deb.nodesource.com/setup_15.x | bash -
|
||||
- apt-get install -y nodejs
|
||||
|
||||
build:
|
||||
image: mcr.microsoft.com/dotnet/sdk:5.0
|
||||
stage: build
|
||||
script:
|
||||
# Set up the environment.
|
||||
- npx npm install --ci
|
||||
- npx commitlint-gitlab-ci -x @commitlint/config-conventional
|
||||
|
||||
# Build and test everything.
|
||||
- dotnet restore
|
||||
- dotnet build
|
||||
#- 'dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"'
|
||||
|
||||
# Summarize the output for Gitlab CI reporting.
|
||||
#- dotnet new tool-manifest
|
||||
#- dotnet tool install dotnet-reportgenerator-globaltool
|
||||
#- dotnet tool run reportgenerator -reports:src/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
|
||||
#- grep "Line coverage" coverage/Summary.txt
|
||||
|
||||
# Perform the release.
|
||||
- npx semantic-release
|
||||
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TITLE =~ /^chore\(release\)/'
|
||||
when: never
|
||||
- if: '$CI_COMMIT_TAG'
|
||||
when: never
|
||||
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
|
||||
when: never
|
||||
- when: on_success
|
||||
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- ./**/*test-result.xml
|
||||
- ./coverage/Cobertura.xml
|
||||
- ./coverage/Summary.*
|
||||
- ./**/*.nupkg
|
||||
reports:
|
||||
junit:
|
||||
- ./**/*test-result.xml
|
||||
cobertura:
|
||||
- ./coverage/Cobertura.xml
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit $1
|
3
.tool-versions
Normal file
3
.tool-versions
Normal file
|
@ -0,0 +1,3 @@
|
|||
dotnet-core 5.0.100
|
||||
yarn 1.22.10
|
||||
nodejs 15.0.1
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Moonfire Games
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
71
MfGames.ToolBuilder.sln
Normal file
71
MfGames.ToolBuilder.sln
Normal file
|
@ -0,0 +1,71 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26124.0
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9C845D9A-B359-43B3-AE9E-B84CE945AF21}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.ToolBuilder", "src\MfGames.ToolBuilder\MfGames.ToolBuilder.csproj", "{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleTool", "tests\SampleTool\SampleTool.csproj", "{0A12108A-9E00-41E9-8935-F4AA7D6208FB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4CE102F8-5C70-4696-B85F-93BB10034918}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.ToolBuilder.Tests", "tests\MfGames.ToolBuilder.Tests\MfGames.ToolBuilder.Tests.csproj", "{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23} = {9C845D9A-B359-43B3-AE9E-B84CE945AF21}
|
||||
{0A12108A-9E00-41E9-8935-F4AA7D6208FB} = {4CE102F8-5C70-4696-B85F-93BB10034918}
|
||||
{77DDD61F-AF57-4AE7-A2A8-08C61A630A9A} = {4CE102F8-5C70-4696-B85F-93BB10034918}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
1370
MfGames.ToolBuilder.sln.DotSettings
Normal file
1370
MfGames.ToolBuilder.sln.DotSettings
Normal file
File diff suppressed because it is too large
Load diff
7
NuGet.Config
Normal file
7
NuGet.Config
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="mfgames" value="https://www.myget.org/F/mfgames/api/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Nitride CIL
|
||||
===========
|
||||
|
||||
A static site generator based on the [Gallium ECS](https://gitlab.com/mfgames-cil/gallium-cil/).
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
};
|
12304
package-lock.json
generated
Normal file
12304
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
package.json
Normal file
21
package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "mfgames-toolbuilder",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^13.1.0",
|
||||
"@commitlint/config-conventional": "^13.1.0",
|
||||
"@semantic-release/changelog": "^5.0.1",
|
||||
"@semantic-release/git": "^9.0.0",
|
||||
"@semantic-release/gitlab": "^6.2.2",
|
||||
"@semantic-release/npm": "^7.1.3",
|
||||
"commitlint-gitlab-ci": "^0.0.4",
|
||||
"husky": "^7.0.2",
|
||||
"semantic-release": "^17.4.7",
|
||||
"semantic-release-dotnet": "^1.0.0",
|
||||
"semantic-release-nuget": "^1.1.0"
|
||||
}
|
||||
}
|
20
release.config.js
Normal file
20
release.config.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
module.exports = {
|
||||
branches: ["main"],
|
||||
message: "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}",
|
||||
plugins: [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/npm",
|
||||
"semantic-release-dotnet",
|
||||
[
|
||||
"semantic-release-nuget",
|
||||
{
|
||||
packArguments: ["--include-symbols", "--include-source"],
|
||||
pushFiles: ["src/*/bin/Debug/*.nupkg"],
|
||||
},
|
||||
],
|
||||
"@semantic-release/changelog",
|
||||
"@semantic-release/git",
|
||||
"@semantic-release/gitlab",
|
||||
],
|
||||
};
|
126
src/MfGames.ToolBuilder/ConfigToolService.cs
Normal file
126
src/MfGames.ToolBuilder/ConfigToolService.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// A utility class for handling `--config` options.
|
||||
/// </summary>
|
||||
public class ConfigToolService
|
||||
{
|
||||
public ConfigToolService()
|
||||
{
|
||||
this.ConfigOption = new Option<string[]>(
|
||||
"--config",
|
||||
"Configuration file to use for settings, otherwise a default will be used.",
|
||||
ArgumentArity.OneOrMore)
|
||||
{
|
||||
AllowMultipleArgumentsPerToken = false,
|
||||
};
|
||||
|
||||
this.ConfigOption.AddAlias("-c");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the common option for setting configuration files.
|
||||
/// </summary>
|
||||
public Option<string[]> ConfigOption { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default configuration file.
|
||||
/// </summary>
|
||||
public FileInfo DefaultConfigFile => new(this.DefaultConfigPath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default configuration path.
|
||||
/// </summary>
|
||||
public string DefaultConfigPath
|
||||
{
|
||||
get
|
||||
{
|
||||
// If we don't have an internal name, blow up.
|
||||
string? internalName = this.InternalName;
|
||||
|
||||
if (internalName == null)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
"Cannot determine the default configuration path unless internal name has been set.");
|
||||
}
|
||||
|
||||
// Figure out the path to the default configuration. This is
|
||||
// something like:
|
||||
// $HOME/.config/ApplicationName/Settings.json
|
||||
string configDirectory = Environment
|
||||
.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
string appDirectory = Path.Combine(
|
||||
configDirectory,
|
||||
internalName);
|
||||
string configPath = Path.Combine(appDirectory, "Settings.json");
|
||||
|
||||
return configPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the application. This is used to figure out
|
||||
/// the name of the configuration file and what is shown on the screen.
|
||||
/// </summary>
|
||||
public string? InternalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the common options to the command.
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
public void AddOptions(RootCommand root)
|
||||
{
|
||||
root.AddGlobalOption(this.ConfigOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up logging based on the global settings.
|
||||
/// </summary>
|
||||
/// <param name="builder">The configuration builder to use.</param>
|
||||
/// <param name="arguments">The arguments to the command.</param>
|
||||
public void Configure(IConfigurationBuilder builder, string[] arguments)
|
||||
{
|
||||
// In general, we don't use a local AppSettings.json automatically
|
||||
// but prefer configuration in the $HOME/.config folder instead.
|
||||
// However, if the user gives a configuration setting, we use that
|
||||
// no matter where we want to put it.
|
||||
string[] configs = GlobalOptionHelper.GetArgumentValue(
|
||||
this.ConfigOption,
|
||||
arguments,
|
||||
Array.Empty<string>());
|
||||
|
||||
// If we don't have anything, then use the default.
|
||||
if (configs.Length == 0)
|
||||
{
|
||||
builder.AddJsonFile(this.DefaultConfigPath, true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, use the default files.
|
||||
foreach (var config in configs)
|
||||
{
|
||||
builder.AddJsonFile(config, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the internal name of the application, used for the
|
||||
/// configuration path.
|
||||
/// </summary>
|
||||
/// <param name="internalName">
|
||||
/// The internal name of the application.
|
||||
/// </param>
|
||||
/// <returns>The service for chaining operations.</returns>
|
||||
public ConfigToolService WithInternalName(string internalName)
|
||||
{
|
||||
this.InternalName = internalName;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
32
src/MfGames.ToolBuilder/Extensions/ParseResultExtensions.cs
Normal file
32
src/MfGames.ToolBuilder/Extensions/ParseResultExtensions.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Linq;
|
||||
|
||||
namespace MfGames.ToolBuilder.Extensions
|
||||
{
|
||||
public static class ParseResultExtensions
|
||||
{
|
||||
public static List<string> ValueListForOption(
|
||||
this ParseResult result,
|
||||
Option<string> option)
|
||||
{
|
||||
string? optionValues = result.ValueForOption(option);
|
||||
|
||||
if (optionValues == null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var values = optionValues
|
||||
.Split(',')
|
||||
.Select(x => x.Trim())
|
||||
.SelectMany(x => x.Split(' '))
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToList();
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
137
src/MfGames.ToolBuilder/Extensions/StringCliExtensions.cs
Normal file
137
src/MfGames.ToolBuilder/Extensions/StringCliExtensions.cs
Normal file
|
@ -0,0 +1,137 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using FluentResults;
|
||||
|
||||
namespace MfGames.ToolBuilder.Extensions
|
||||
{
|
||||
public static class StringCliExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a string and converts it into an enumeration while ignoring
|
||||
/// case and allow only prefix values. If the value cannot be
|
||||
/// determined, it will report it and throw an exception.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enumeration to parse.</typeparam>
|
||||
/// <param name="input">The input value to parse.</param>
|
||||
/// <param name="logger">The logger to report issues.</param>
|
||||
/// <param name="label">The name of the variable or label.</param>
|
||||
/// <returns></returns>
|
||||
public static TEnum GetEnumFuzzy<TEnum>(
|
||||
this string input,
|
||||
string label)
|
||||
where TEnum : struct
|
||||
{
|
||||
if (input.TryParseEnumFuzzy(out TEnum value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
string.Format(
|
||||
"Cannot parse unknown value {0} for {1}: {2}",
|
||||
input,
|
||||
label,
|
||||
Enum.GetNames(typeof(TEnum))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for possible list of values for a given one, handling
|
||||
/// case-insensitive searching and substring searches.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to search for.</param>
|
||||
/// <param name="possible">A collection of all possible values.</param>
|
||||
/// <returns>A tuple with the selected value or an error message.</returns>
|
||||
public static Result<string> GetFuzzy(
|
||||
this string value,
|
||||
ICollection<string> possible)
|
||||
{
|
||||
// Look for a direct match.
|
||||
List<string> found = possible
|
||||
.Where(
|
||||
x => string.Equals(
|
||||
x,
|
||||
value,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (found.Count == 1)
|
||||
{
|
||||
return Result.Ok(found[0]);
|
||||
}
|
||||
|
||||
// Look for substrings in the values.
|
||||
found = possible
|
||||
.Where(
|
||||
x => x.Contains(
|
||||
value,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return found.Count switch
|
||||
{
|
||||
1 => Result.Ok(found[0]),
|
||||
0 => Result.Fail<string>(
|
||||
string.Format(
|
||||
"Cannot find \"{0}\" from possible options: {1}.",
|
||||
value,
|
||||
string.Join(", ", possible))),
|
||||
_ => Result.Fail<string>(
|
||||
string.Format(
|
||||
"Found multiple matches for \"{0}\" from possible options: {1}.",
|
||||
value,
|
||||
string.Join(", ", found))),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string and converts it into an enumeration while ignoring
|
||||
/// case and allowing only prefix
|
||||
/// values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enumeration to parse.</typeparam>
|
||||
/// <param name="input">The input value to parse.</param>
|
||||
/// <param name="value">
|
||||
/// The resulting value if parsed or default if not.
|
||||
/// </param>
|
||||
/// <returns>The resulting enum.</returns>
|
||||
public static bool TryParseEnumFuzzy<TEnum>(
|
||||
this string input,
|
||||
out TEnum value)
|
||||
where TEnum : struct
|
||||
{
|
||||
// If we have a blank, then we don't know what to do.
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
value = default;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// See if we have an exact match first.
|
||||
if (Enum.TryParse(input, true, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt a fuzzy search.
|
||||
List<string> possible = Enum.GetNames(typeof(TEnum))
|
||||
.Select(e => e.ToLower())
|
||||
.Where(e => e.StartsWith(input.ToLower()))
|
||||
.ToList();
|
||||
|
||||
if (possible.Count == 1)
|
||||
{
|
||||
value = (TEnum)Enum.Parse(typeof(TEnum), possible[0], true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fall back to not allowing the value.
|
||||
value = default;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
35
src/MfGames.ToolBuilder/GlobalOptionHelper.cs
Normal file
35
src/MfGames.ToolBuilder/GlobalOptionHelper.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper methods for pulling out values without performing the full parse.
|
||||
/// </summary>
|
||||
public static class GlobalOptionHelper
|
||||
{
|
||||
public static TType GetArgumentValue<TType>(
|
||||
Option<TType> option,
|
||||
string[] arguments,
|
||||
TType defaultValue)
|
||||
{
|
||||
var rootCommand = new RootCommand
|
||||
{
|
||||
option,
|
||||
};
|
||||
|
||||
rootCommand.TreatUnmatchedTokensAsErrors = false;
|
||||
|
||||
ParseResult results = rootCommand.Parse(arguments);
|
||||
|
||||
if (!results.HasOption(option))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
TType value = results.ValueForOption(option)!;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
14
src/MfGames.ToolBuilder/ITopCommand.cs
Normal file
14
src/MfGames.ToolBuilder/ITopCommand.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System.CommandLine;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface that indicates that the given command is a top-level
|
||||
/// command instead of one that is included as a sub-command inside another.
|
||||
/// This is used to arrange the various sub-commands using dependency
|
||||
/// injection and is purely a marker interface.
|
||||
/// </summary>
|
||||
public interface ITopCommand : ICommand
|
||||
{
|
||||
}
|
||||
}
|
77
src/MfGames.ToolBuilder/LoggingToolService.cs
Normal file
77
src/MfGames.ToolBuilder/LoggingToolService.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
|
||||
using MfGames.ToolBuilder.Extensions;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Exceptions;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// A service for handling logging options.
|
||||
/// </summary>
|
||||
public class LoggingToolService
|
||||
{
|
||||
public LoggingToolService()
|
||||
{
|
||||
this.LogLevelOption = new Option<string>(
|
||||
"--log-level",
|
||||
() => nameof(LogEventLevel.Information),
|
||||
string.Format(
|
||||
"Controls the verbosity of the output: {0}. Not case-sensitive and prefixes allowed.",
|
||||
string.Join(", ", Enum.GetNames<LogEventLevel>())));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the common option for setting the log level.
|
||||
/// </summary>
|
||||
public Option<string> LogLevelOption { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the common options to the command.
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
public void AddOptions(RootCommand root)
|
||||
{
|
||||
root.AddGlobalOption(this.LogLevelOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up logging based on the global settings.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments to the command.</param>
|
||||
public void Configure(string[] arguments)
|
||||
{
|
||||
// Figure out the logging level.
|
||||
string level = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogLevelOption,
|
||||
arguments,
|
||||
"Information");
|
||||
LogEventLevel logLevel = level.GetEnumFuzzy<LogEventLevel>(
|
||||
"log level");
|
||||
|
||||
// Figure out how we are going to configure the logger.
|
||||
var configuration = new LoggerConfiguration();
|
||||
|
||||
// Create the logger and set it.
|
||||
const string template =
|
||||
"{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] "
|
||||
+ "{Message}"
|
||||
+ "{NewLine}{Exception}";
|
||||
|
||||
Logger logger = configuration
|
||||
.Enrich.WithDemystifiedStackTraces()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.MinimumLevel.Is(logLevel)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(outputTemplate: template)
|
||||
.CreateLogger();
|
||||
|
||||
Log.Logger = logger;
|
||||
}
|
||||
}
|
||||
}
|
42
src/MfGames.ToolBuilder/MfGames.ToolBuilder.csproj
Normal file
42
src/MfGames.ToolBuilder/MfGames.ToolBuilder.csproj
Normal file
|
@ -0,0 +1,42 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<Description>A framework for easily creating command line tools using System.CommandLine, Autofac, and Serilog.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="6.2.0" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.1.0" />
|
||||
<PackageReference Include="ConsoleTableExt" Version="3.1.9" />
|
||||
<PackageReference Include="CsvHelper" Version="27.1.1" />
|
||||
<PackageReference Include="FluentResults" Version="2.5.0" />
|
||||
<PackageReference Include="Glob" Version="1.1.8" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.11.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="3.2.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="1.1.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="1.2.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Demystifier" Version="1.0.1" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Autofac.DependencyInjection" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
50
src/MfGames.ToolBuilder/Tables/TableFormatType.cs
Normal file
50
src/MfGames.ToolBuilder/Tables/TableFormatType.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
namespace MfGames.ToolBuilder.Tables
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of the known formats. This duplicates the values in
|
||||
/// `ConsoleTableBuilderFormat` in addition to adding
|
||||
/// our additional formats.
|
||||
/// </summary>
|
||||
public enum TableFormatType
|
||||
{
|
||||
/// <summary>
|
||||
/// Duplicates ConsoleTableBuilderFormat.Default.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates ConsoleTableBuilderFormat.MarkDown.
|
||||
/// </summary>
|
||||
Markdown,
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates ConsoleTableBuilderFormat.Alternative.
|
||||
/// </summary>
|
||||
Alternative,
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates ConsoleTableBuilderFormat.Minimal.
|
||||
/// </summary>
|
||||
Minimal,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the output should be written as a JSON structure.
|
||||
/// </summary>
|
||||
Json,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the output should be written as a PowerShell-style list.
|
||||
/// </summary>
|
||||
List,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates tht the output should be written as comma-separated values.
|
||||
/// </summary>
|
||||
Csv,
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates ConsoleTableBuilderFormat.Default.
|
||||
/// </summary>
|
||||
Full = Default,
|
||||
}
|
||||
}
|
437
src/MfGames.ToolBuilder/Tables/TableToolService.cs
Normal file
437
src/MfGames.ToolBuilder/Tables/TableToolService.cs
Normal file
|
@ -0,0 +1,437 @@
|
|||
#pragma warning disable Serilog004 // Constant MessageTemplate verifier
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.IO;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using ConsoleTableExt;
|
||||
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
|
||||
using FluentResults;
|
||||
|
||||
using GlobExpressions;
|
||||
|
||||
using MfGames.ToolBuilder.Extensions;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace MfGames.ToolBuilder.Tables
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains various utility functions for working with commands that display
|
||||
/// tables.
|
||||
/// </summary>
|
||||
public class TableToolService
|
||||
{
|
||||
private readonly Command command;
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly Option<bool> noAlignTableOption;
|
||||
|
||||
private readonly Option<string> tableColumnOption;
|
||||
|
||||
private readonly Option<string> tableFormatOption;
|
||||
|
||||
private DataTable? table;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new table CLI service with specific commands.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger for requests.</param>
|
||||
/// <param name="command">The command to add the options to.</param>
|
||||
/// <param name="table">The optional table to columns the parameters.</param>
|
||||
/// <param name="defaultColumns">A list of columns to display if none are set.</param>
|
||||
public TableToolService(
|
||||
ILogger logger,
|
||||
Command command,
|
||||
DataTable? table = null,
|
||||
IList<string>? defaultColumns = null)
|
||||
{
|
||||
// Set member variables.
|
||||
this.logger = logger;
|
||||
this.command = command;
|
||||
this.table = table;
|
||||
this.DefaultColumns = defaultColumns;
|
||||
|
||||
// If we have a table, then we can show the list of known columns.
|
||||
string? tableColumnsDescription =
|
||||
"The columns to display in the output.";
|
||||
|
||||
string? tableFormatDescription = string.Format(
|
||||
"The fuzzy format of the table, one of: {0}.",
|
||||
string.Join(", ", Enum.GetNames<TableFormatType>()));
|
||||
|
||||
if (table != null)
|
||||
{
|
||||
tableColumnsDescription = string.Format(
|
||||
"The columns to display in the output, \"*\" or comma-separated list of: {0}.",
|
||||
string.Join(
|
||||
", ",
|
||||
table.Columns.OfType<DataColumn>()
|
||||
.Select(x => x.ColumnName)));
|
||||
|
||||
if (defaultColumns != null)
|
||||
{
|
||||
tableColumnsDescription += " [default: "
|
||||
+ string.Join(",", defaultColumns)
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
// Create the parameters for the table.
|
||||
this.tableColumnOption = new Option<string>(
|
||||
"--table-columns",
|
||||
tableColumnsDescription)
|
||||
{
|
||||
ArgumentHelpName = "column[,column...]",
|
||||
};
|
||||
|
||||
this.noAlignTableOption = new Option<bool>(
|
||||
"--no-align-table-columns",
|
||||
() => false,
|
||||
"If set, don't right-align numerical columns.");
|
||||
|
||||
this.tableFormatOption = new Option<string>(
|
||||
"--table-format",
|
||||
() => nameof(TableFormatType.Minimal),
|
||||
tableFormatDescription)
|
||||
{
|
||||
ArgumentHelpName = "format",
|
||||
};
|
||||
|
||||
// Add the options into the command.
|
||||
command.AddOption(this.tableFormatOption);
|
||||
command.AddOption(this.noAlignTableOption);
|
||||
command.AddOption(this.tableColumnOption);
|
||||
}
|
||||
|
||||
public delegate TableToolService Factory(
|
||||
Command command,
|
||||
DataTable? table = null,
|
||||
IList<string>? defaultColumns = null);
|
||||
|
||||
public IList<string>? DefaultColumns { get; set; }
|
||||
|
||||
public IList<string> GetVisibleColumnNames(InvocationContext context)
|
||||
{
|
||||
// Do a little sanity checking on member variables.
|
||||
if (this.table == null)
|
||||
{
|
||||
throw new NullReferenceException(
|
||||
"The table field has not been defined.");
|
||||
}
|
||||
|
||||
// Figure out the columns that the user wants to see. If they have
|
||||
// not specified any, then we use the defaults.
|
||||
List<string> defaultColumns =
|
||||
this.DefaultColumns?.ToList() ?? new List<string>();
|
||||
|
||||
var optionColumns = context.ParseResult
|
||||
.ValueListForOption(this.tableColumnOption)
|
||||
.ToList();
|
||||
|
||||
List<string> tableColumns = optionColumns.Count == 0
|
||||
? defaultColumns
|
||||
: optionColumns;
|
||||
|
||||
// Expand the columns out to handle a simplified globbing where "*"
|
||||
// means include all columns.
|
||||
var allColumns = this.table.Columns
|
||||
.Cast<DataColumn>()
|
||||
.Select(x => x.ColumnName)
|
||||
.ToList();
|
||||
|
||||
List<string> expandedNames = tableColumns
|
||||
.SelectMany(x => IsMatch(allColumns, x))
|
||||
.ToList();
|
||||
|
||||
// Do a fuzzy matching of names. This allows for a case-insensitive
|
||||
// search while also allowing for prefixes (so "d" works for
|
||||
// "Debug").
|
||||
List<Result<string>> fuzzyResults = expandedNames
|
||||
.ConvertAll(x => x.GetFuzzy(allColumns));
|
||||
|
||||
ToolException.ThrowIf(fuzzyResults);
|
||||
|
||||
var fuzzyValues = fuzzyResults
|
||||
.Select(x => x.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return fuzzyValues;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the column is going to be shown to the user.
|
||||
/// </summary>
|
||||
/// <param name="context">The context of the request.</param>
|
||||
/// <param name="columnName">The column to query.</param>
|
||||
/// <returns>True if the column is visible, otherwise false.</returns>
|
||||
public bool IsVisible(InvocationContext context, string columnName)
|
||||
{
|
||||
if (this.table == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot use IsVisible without a sample table given.");
|
||||
}
|
||||
|
||||
IList<string> visibleColumns = this.GetVisibleColumnNames(context);
|
||||
|
||||
bool visible = visibleColumns
|
||||
.Any(
|
||||
x => x.Equals(
|
||||
columnName,
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders out the table using the requested formats.
|
||||
/// </summary>
|
||||
/// <param name="context">The context of the command.</param>
|
||||
/// <param name="updatedTable">The table to use if not the initial one..</param>
|
||||
public void Write(
|
||||
InvocationContext context,
|
||||
DataTable? updatedTable = null)
|
||||
{
|
||||
// Get the table and make sure we have it.
|
||||
this.table = updatedTable ?? this.table;
|
||||
|
||||
if (this.table == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot write out a table if not given in the constructor, updated via property, or passed into the write method.");
|
||||
}
|
||||
|
||||
// Adjust, hide, and reorder columns.
|
||||
if (!this.AdjustColumns(context))
|
||||
{
|
||||
throw new ToolException(
|
||||
"Cannot adjust the columns of the resulting data");
|
||||
}
|
||||
|
||||
// Pass the resulting data to the formatting code.
|
||||
TableFormatType tableFormat = this.GetTableFormat(context);
|
||||
|
||||
switch (tableFormat)
|
||||
{
|
||||
case TableFormatType.Default:
|
||||
case TableFormatType.Markdown:
|
||||
case TableFormatType.Alternative:
|
||||
case TableFormatType.Minimal:
|
||||
this.WriteTable(context, tableFormat);
|
||||
|
||||
break;
|
||||
|
||||
case TableFormatType.Json:
|
||||
this.WriteJson(context);
|
||||
|
||||
break;
|
||||
|
||||
case TableFormatType.List:
|
||||
this.WriteList(context);
|
||||
|
||||
break;
|
||||
|
||||
case TableFormatType.Csv:
|
||||
this.WriteCsv(context);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> IsMatch(
|
||||
IEnumerable<string> columns,
|
||||
string input)
|
||||
{
|
||||
var glob = new Glob(input, GlobOptions.CaseInsensitive);
|
||||
var matches = columns
|
||||
.Where(x => glob.IsMatch(x))
|
||||
.ToList();
|
||||
|
||||
if (matches.Count > 0)
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
|
||||
return new[] { input };
|
||||
}
|
||||
|
||||
private bool AdjustColumns(InvocationContext context)
|
||||
{
|
||||
// Figure out if we need to change the columns.
|
||||
IList<string> columnNames = this.GetVisibleColumnNames(context);
|
||||
|
||||
if (columnNames.Count == 0)
|
||||
{
|
||||
this.logger.Error("There were no columns to display");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove excessive columns.
|
||||
IEnumerable<DataColumn> columnsToRemove = this.table!.Columns
|
||||
.OfType<DataColumn>()
|
||||
.Where(x => !columnNames.Contains(x.ColumnName))
|
||||
.ToList();
|
||||
|
||||
foreach (DataColumn column in columnsToRemove)
|
||||
{
|
||||
this.table.Columns.Remove(column);
|
||||
}
|
||||
|
||||
// Reorder the columns to match requested order.
|
||||
for (int order = 0; order < columnNames.Count; order++)
|
||||
{
|
||||
this.table.Columns[columnNames[order]]!.SetOrdinal(order);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private TableFormatType GetTableFormat(InvocationContext context)
|
||||
{
|
||||
string? tableFormat =
|
||||
context.ParseResult.ValueForOption(this.tableFormatOption)
|
||||
?? nameof(TableFormatType.Minimal);
|
||||
|
||||
if (!tableFormat.TryParseEnumFuzzy(out TableFormatType value))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unknown table format '"
|
||||
+ tableFormat
|
||||
+ "'. Must be one of: "
|
||||
+ string.Join(", ", Enum.GetNames<TableFormatType>()));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes out the contents of the table as CSV.
|
||||
/// </summary>
|
||||
private void WriteCsv(InvocationContext context)
|
||||
{
|
||||
var configuration =
|
||||
new CsvConfiguration(CultureInfo.CurrentCulture);
|
||||
var stringWriter = new StringWriter();
|
||||
var csv = new CsvWriter(stringWriter, configuration);
|
||||
|
||||
foreach (DataColumn column in
|
||||
this.table!.Columns.OfType<DataColumn>())
|
||||
{
|
||||
csv.WriteField(column.ColumnName);
|
||||
}
|
||||
|
||||
csv.NextRecord();
|
||||
|
||||
foreach (DataRow row in this.table.AsEnumerable())
|
||||
{
|
||||
foreach (DataColumn column in this.table.Columns
|
||||
.OfType<DataColumn>())
|
||||
{
|
||||
csv.WriteField(Convert.ToString(row[column]));
|
||||
}
|
||||
|
||||
csv.NextRecord();
|
||||
}
|
||||
|
||||
context.Console.Out.Write(stringWriter.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes out the contents of the table as JSON.
|
||||
/// </summary>
|
||||
private void WriteJson(InvocationContext context)
|
||||
{
|
||||
context.Console.Out.WriteLine(
|
||||
JsonConvert.SerializeObject(this.table));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes out the contents of the table in a PowerShell-like list.
|
||||
/// </summary>
|
||||
private void WriteList(InvocationContext context)
|
||||
{
|
||||
int labelWidth = this.table!.Columns
|
||||
.OfType<DataColumn>()
|
||||
.Max(x => x.ColumnName.Length);
|
||||
|
||||
bool first = true;
|
||||
|
||||
foreach (DataRow row in this.table.AsEnumerable())
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Console.Out.WriteLine();
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.table.Columns.Count; i++)
|
||||
{
|
||||
context.Console.Out.WriteLine(
|
||||
string.Format(
|
||||
"{0} : {1}",
|
||||
this.table.Columns[i]
|
||||
.ColumnName.PadRight(labelWidth),
|
||||
row[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteTable(
|
||||
InvocationContext context,
|
||||
TableFormatType tableFormat)
|
||||
{
|
||||
// Build the table from options.
|
||||
ConsoleTableBuilder builder = ConsoleTableBuilder
|
||||
.From(this.table)
|
||||
.WithFormat((ConsoleTableBuilderFormat)tableFormat);
|
||||
|
||||
// We default to aligning numerical columns to the right.
|
||||
bool noAlign =
|
||||
context.ParseResult.ValueForOption(this.noAlignTableOption);
|
||||
|
||||
if (!noAlign)
|
||||
{
|
||||
builder.WithTextAlignment(
|
||||
this.table!.Columns
|
||||
.OfType<DataColumn>()
|
||||
.Select(
|
||||
(column, index) => (Index: index,
|
||||
Numeric: column.DataType.Name switch
|
||||
{
|
||||
nameof(Byte) => true,
|
||||
nameof(Int16) => true,
|
||||
nameof(Int32) => true,
|
||||
nameof(Int64) => true,
|
||||
nameof(Decimal) => true,
|
||||
_ => false,
|
||||
}))
|
||||
.Where(x => x.Numeric)
|
||||
.ToDictionary(x => x.Index, x => TextAligntment.Right));
|
||||
}
|
||||
|
||||
// Write out the results.
|
||||
context.Console.Out.Write(builder.Export().ToString());
|
||||
}
|
||||
}
|
||||
}
|
151
src/MfGames.ToolBuilder/ToolBuilder.cs
Normal file
151
src/MfGames.ToolBuilder/ToolBuilder.cs
Normal file
|
@ -0,0 +1,151 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder pattern for creating the tool. This wraps much of the hosting
|
||||
/// infrastructure with some opinionated decisions and reduces the amount of
|
||||
/// boilerplate needed to configure the tool.
|
||||
/// </summary>
|
||||
public class ToolBuilder
|
||||
{
|
||||
private readonly string[] arguments;
|
||||
|
||||
private readonly ConfigToolService configService;
|
||||
|
||||
private readonly IHostBuilder hostBuilder;
|
||||
|
||||
private readonly LoggingToolService loggingService;
|
||||
|
||||
public ToolBuilder(
|
||||
string applicationName,
|
||||
string internalName,
|
||||
string[] arguments)
|
||||
{
|
||||
// Create our various services.
|
||||
this.arguments = arguments;
|
||||
this.ApplicationName = applicationName;
|
||||
this.InternalName = internalName;
|
||||
this.configService = new ConfigToolService()
|
||||
.WithInternalName(this.InternalName);
|
||||
this.loggingService = new LoggingToolService();
|
||||
|
||||
// Set up logging first so we can report the loading process. This
|
||||
// sets up the Serilog.Log.Logger which means we can use that for
|
||||
// everything beyond this point.
|
||||
this.loggingService.Configure(arguments);
|
||||
|
||||
// Start up the basic configuration.
|
||||
this.hostBuilder = Host
|
||||
.CreateDefaultBuilder(arguments)
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureAppConfiguration(this.ConfigureAppConfiguration)
|
||||
.UseSerilog()
|
||||
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
|
||||
.ConfigureServices(this.ConfigureServices)
|
||||
.ConfigureContainer<ContainerBuilder>(this.ConfigureContainer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the human-readable name of the application.
|
||||
/// </summary>
|
||||
public string ApplicationName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the internal name of the application.
|
||||
/// </summary>
|
||||
public string InternalName { get; }
|
||||
|
||||
public static ToolBuilder Create(
|
||||
string applicationName,
|
||||
string internalName,
|
||||
string[] arguments)
|
||||
{
|
||||
return new ToolBuilder(applicationName, internalName, arguments);
|
||||
}
|
||||
|
||||
public ToolBuilder ConfigureContainer(
|
||||
Action<ContainerBuilder> configure)
|
||||
{
|
||||
this.hostBuilder.ConfigureContainer(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolBuilder ConfigureServices(
|
||||
Action<IServiceCollection> configure)
|
||||
{
|
||||
this.hostBuilder.ConfigureServices(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes building the tool, parses the arguments, and runs the
|
||||
/// command.
|
||||
/// </summary>
|
||||
/// <returns>An error code, 0 for successful, otherwise false.</returns>
|
||||
public async Task<int> RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.hostBuilder
|
||||
.RunConsoleAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Fatal(
|
||||
exception,
|
||||
"There was a problem running the command: {Arguments}",
|
||||
this.arguments);
|
||||
|
||||
return Environment.ExitCode == 0 ? 1 : Environment.ExitCode;
|
||||
}
|
||||
|
||||
// Get the exit code and return it.
|
||||
return Environment.ExitCode;
|
||||
}
|
||||
|
||||
private void ConfigureAppConfiguration(
|
||||
HostBuilderContext context,
|
||||
IConfigurationBuilder builder)
|
||||
{
|
||||
builder.SetBasePath(Directory.GetCurrentDirectory());
|
||||
this.configService.Configure(builder, this.arguments);
|
||||
}
|
||||
|
||||
private void ConfigureContainer(
|
||||
HostBuilderContext context,
|
||||
ContainerBuilder builder)
|
||||
{
|
||||
// We want to get logging up and running as soon as possible. We
|
||||
// also hook up the logging to the process exit in an attempt to
|
||||
// make sure the logger is properly flushed before exiting.
|
||||
builder.RegisterInstance(Log.Logger).As<ILogger>().SingleInstance();
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit +=
|
||||
(_, _) => Log.CloseAndFlush();
|
||||
|
||||
// Register the components required to make the CLI work.
|
||||
builder.RegisterModule<ToolBuilderModule>();
|
||||
}
|
||||
|
||||
private void ConfigureServices(
|
||||
HostBuilderContext context,
|
||||
IServiceCollection services)
|
||||
{
|
||||
services.AddAutofac();
|
||||
services.AddHostedService<ToolService>();
|
||||
}
|
||||
}
|
||||
}
|
20
src/MfGames.ToolBuilder/ToolBuilderModule.cs
Normal file
20
src/MfGames.ToolBuilder/ToolBuilderModule.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using Autofac;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The Autofac module to pull in the components inside this assembly.
|
||||
/// </summary>
|
||||
public class ToolBuilderModule : Module
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder
|
||||
.RegisterAssemblyTypes(this.GetType().Assembly)
|
||||
.Except<ToolService>()
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces();
|
||||
}
|
||||
}
|
||||
}
|
70
src/MfGames.ToolBuilder/ToolException.cs
Normal file
70
src/MfGames.ToolBuilder/ToolException.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using FluentResults;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
public class ToolException : Exception
|
||||
{
|
||||
public ToolException(IEnumerable<string> messages)
|
||||
: this(messages.ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
public ToolException(params string[] messages)
|
||||
: base("There was an exception while processing the tool.")
|
||||
{
|
||||
this.Messages = messages;
|
||||
}
|
||||
|
||||
public ToolException()
|
||||
: base("There was an exception while processing the tool.")
|
||||
{
|
||||
this.Messages = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public ToolException(string? message)
|
||||
: this()
|
||||
{
|
||||
this.Messages = message != null
|
||||
? new[] { message }
|
||||
: Array.Empty<string>();
|
||||
}
|
||||
|
||||
public ToolException(string? message, Exception? innerException)
|
||||
: base(
|
||||
"There was an exception while processing the tool.",
|
||||
innerException)
|
||||
{
|
||||
this.Messages = message != null
|
||||
? new[] { message }
|
||||
: Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int ExitCode { get; } = 1;
|
||||
|
||||
public string[] Messages { get; }
|
||||
|
||||
public bool SuppressStackTrace { get; set; } = true;
|
||||
|
||||
public static void ThrowIf(IEnumerable<ResultBase> results)
|
||||
{
|
||||
var errors = results
|
||||
.Where(x => x.IsFailed)
|
||||
.ToList();
|
||||
|
||||
if (errors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var messages = errors
|
||||
.SelectMany(x => x.Errors)
|
||||
.Select(x => x.Message);
|
||||
|
||||
throw new ToolException(messages);
|
||||
}
|
||||
}
|
||||
}
|
139
src/MfGames.ToolBuilder/ToolService.cs
Normal file
139
src/MfGames.ToolBuilder/ToolService.cs
Normal file
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Serilog;
|
||||
|
||||
#pragma warning disable Serilog004
|
||||
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
// ReSharper disable SuspiciousTypeConversion.Global
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the command-line shell for generating the static site.
|
||||
/// </summary>
|
||||
public class ToolService : IHostedService
|
||||
{
|
||||
private readonly IList<ITopCommand> commands;
|
||||
|
||||
private readonly ConfigToolService configService;
|
||||
|
||||
private readonly IHostApplicationLifetime lifetime;
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly LoggingToolService loggingService;
|
||||
|
||||
public ToolService(
|
||||
ILogger logger,
|
||||
IHostApplicationLifetime lifetime,
|
||||
IList<ITopCommand> commands,
|
||||
ConfigToolService configService,
|
||||
LoggingToolService loggingService)
|
||||
{
|
||||
this.lifetime = lifetime;
|
||||
this.commands = commands;
|
||||
this.configService = configService;
|
||||
this.loggingService = loggingService;
|
||||
this.logger = logger.ForContext<ToolService>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.lifetime.ApplicationStarted
|
||||
.Register(
|
||||
() =>
|
||||
{
|
||||
Task.Run(
|
||||
async () =>
|
||||
await this.RunAsync().ConfigureAwait(false),
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private RootCommand CreateRootCommand()
|
||||
{
|
||||
// Create the root command and add in the top-level commands
|
||||
// underneath it.
|
||||
var root = new RootCommand();
|
||||
|
||||
foreach (var command in this.commands)
|
||||
{
|
||||
root.AddCommand((Command)command);
|
||||
}
|
||||
|
||||
// Add the universal options.
|
||||
this.loggingService.AddOptions(root);
|
||||
this.configService.AddOptions(root);
|
||||
|
||||
// Return the resulting container.
|
||||
return root;
|
||||
}
|
||||
|
||||
private void OnException(Exception exception, InvocationContext context)
|
||||
{
|
||||
if (exception is ToolException toolException)
|
||||
{
|
||||
this.logger.Fatal(toolException.Message);
|
||||
|
||||
foreach (var message in toolException.Messages)
|
||||
{
|
||||
this.logger.Fatal(message);
|
||||
}
|
||||
|
||||
Environment.ExitCode = toolException.ExitCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.Fatal(
|
||||
exception,
|
||||
"Unhandled exception!");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build the command tree.
|
||||
RootCommand root = this.CreateRootCommand();
|
||||
string[] args = Environment.GetCommandLineArgs();
|
||||
|
||||
// Execute the command.
|
||||
this.logger.Verbose(
|
||||
"Running the command-line arguments: {Arguments}",
|
||||
args);
|
||||
|
||||
Environment.ExitCode = await new CommandLineBuilder(root)
|
||||
.UseDefaults()
|
||||
.UseExceptionHandler(this.OnException)
|
||||
.Build()
|
||||
.InvokeAsync(args)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Stop the application once the work is done.
|
||||
this.lifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="JunitXml.TestLogger" Version="2.1.81" />
|
||||
<PackageReference Include="MfGames.TestSetup" Version="1.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="3.2.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="1.1.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="1.2.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MfGames.ToolBuilder\MfGames.ToolBuilder.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="MfGames.TestSetup, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
|
||||
<HintPath>bin\Debug\net5.0\MfGames.TestSetup.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
151
tests/MfGames.ToolBuilder.Tests/TableToolServiceTests.cs
Normal file
151
tests/MfGames.ToolBuilder.Tests/TableToolServiceTests.cs
Normal file
|
@ -0,0 +1,151 @@
|
|||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Data;
|
||||
|
||||
using MfGames.TestSetup;
|
||||
using MfGames.ToolBuilder.Tables;
|
||||
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace MfGames.ToolBuilder.Tests
|
||||
{
|
||||
public class TableToolServiceTests : TestBase<ToolBuilderTestContext>
|
||||
{
|
||||
private readonly DataTable table;
|
||||
|
||||
public TableToolServiceTests(ITestOutputHelper output)
|
||||
: base(output)
|
||||
{
|
||||
// Create the table structure.
|
||||
this.table = new DataTable();
|
||||
this.table.Columns.Add("DefaultString", typeof(string));
|
||||
this.table.Columns.Add("DefaultInt32", typeof(int));
|
||||
this.table.Columns.Add("HiddenString", typeof(string));
|
||||
this.table.Rows.Add("Row 1", 1, "Hidden 1");
|
||||
this.table.Rows.Add("Row 2", 10, "Hidden 2");
|
||||
this.table.Rows.Add("Row 3", 100, "Hidden 3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseAll()
|
||||
{
|
||||
// Set up the test.
|
||||
using ToolBuilderTestContext context = this.CreateContext();
|
||||
|
||||
// Create the command with the table.
|
||||
(var service, InvocationContext invoke) = this.InvokeCommand(
|
||||
context,
|
||||
"--table-columns",
|
||||
"*");
|
||||
|
||||
// Verify the results.
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"DefaultString",
|
||||
"DefaultInt32",
|
||||
"HiddenString",
|
||||
},
|
||||
service.GetVisibleColumnNames(invoke));
|
||||
Assert.True(service.IsVisible(invoke, "DefaultString"));
|
||||
Assert.True(service.IsVisible(invoke, "DefaultInt32"));
|
||||
Assert.True(service.IsVisible(invoke, "HiddenString"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseExactMatch()
|
||||
{
|
||||
// Set up the test.
|
||||
using ToolBuilderTestContext context = this.CreateContext();
|
||||
|
||||
// Create the command with the table.
|
||||
(var service, InvocationContext invoke) = this.InvokeCommand(
|
||||
context,
|
||||
"--table-columns",
|
||||
"DefaultInt32");
|
||||
|
||||
// Verify the results.
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"DefaultInt32",
|
||||
},
|
||||
service.GetVisibleColumnNames(invoke));
|
||||
Assert.False(service.IsVisible(invoke, "DefaultString"));
|
||||
Assert.True(service.IsVisible(invoke, "DefaultInt32"));
|
||||
Assert.False(service.IsVisible(invoke, "HiddenString"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseGlob()
|
||||
{
|
||||
// Set up the test.
|
||||
using ToolBuilderTestContext context = this.CreateContext();
|
||||
|
||||
// Create the command with the table.
|
||||
(var service, InvocationContext invoke) = this.InvokeCommand(
|
||||
context,
|
||||
"--table-columns",
|
||||
"*string");
|
||||
|
||||
// Verify the results.
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"DefaultString",
|
||||
"HiddenString",
|
||||
},
|
||||
service.GetVisibleColumnNames(invoke));
|
||||
Assert.True(service.IsVisible(invoke, "DefaultString"));
|
||||
Assert.False(service.IsVisible(invoke, "DefaultInt32"));
|
||||
Assert.True(service.IsVisible(invoke, "HiddenString"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultColumns()
|
||||
{
|
||||
// Set up the test.
|
||||
using ToolBuilderTestContext context = this.CreateContext();
|
||||
|
||||
// Create the command with the table.
|
||||
(var service, InvocationContext invoke) =
|
||||
this.InvokeCommand(context);
|
||||
|
||||
// Verify the results.
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
"DefaultString",
|
||||
"DefaultInt32",
|
||||
},
|
||||
service.GetVisibleColumnNames(invoke));
|
||||
Assert.True(service.IsVisible(invoke, "DefaultString"));
|
||||
Assert.True(service.IsVisible(invoke, "DefaultInt32"));
|
||||
Assert.False(service.IsVisible(invoke, "HiddenString"));
|
||||
}
|
||||
|
||||
private (TableToolService service, InvocationContext invoke)
|
||||
InvokeCommand(
|
||||
ToolBuilderTestContext context,
|
||||
params string[] arguments)
|
||||
{
|
||||
TableToolService.Factory serviceFactory =
|
||||
context.Resolve<TableToolService.Factory>();
|
||||
var command = new RootCommand();
|
||||
TableToolService service = serviceFactory(
|
||||
command,
|
||||
this.table,
|
||||
new List<string>
|
||||
{
|
||||
"DefaultString",
|
||||
"DefaultInt32",
|
||||
});
|
||||
ParseResult results = command.Parse(arguments);
|
||||
var invoke = new InvocationContext(results);
|
||||
return (service, invoke);
|
||||
}
|
||||
}
|
||||
}
|
16
tests/MfGames.ToolBuilder.Tests/ToolBuilderTestContext.cs
Normal file
16
tests/MfGames.ToolBuilder.Tests/ToolBuilderTestContext.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using Autofac;
|
||||
|
||||
using MfGames.TestSetup;
|
||||
|
||||
namespace MfGames.ToolBuilder.Tests
|
||||
{
|
||||
public class ToolBuilderTestContext : TestContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureContainer(ContainerBuilder builder)
|
||||
{
|
||||
base.ConfigureContainer(builder);
|
||||
builder.RegisterModule<ToolBuilderModule>();
|
||||
}
|
||||
}
|
||||
}
|
38
tests/SampleTool/CrashTopCommand.cs
Normal file
38
tests/SampleTool/CrashTopCommand.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using MfGames.ToolBuilder;
|
||||
|
||||
namespace SampleTool
|
||||
{
|
||||
public class CrashTopCommand : Command, ITopCommand, ICommandHandler
|
||||
{
|
||||
private readonly Option<bool> messyOption;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CrashTopCommand()
|
||||
: base("crash", "Crash the application with an exception.")
|
||||
{
|
||||
this.Handler = this;
|
||||
this.messyOption = new Option<bool>("--messy");
|
||||
|
||||
this.AddOption(this.messyOption);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> InvokeAsync(InvocationContext context)
|
||||
{
|
||||
bool messy = context.ParseResult.ValueForOption(this.messyOption);
|
||||
|
||||
if (messy)
|
||||
{
|
||||
throw new Exception(
|
||||
"This command crashed messily as requested.");
|
||||
}
|
||||
|
||||
throw new ToolException("This command crashed as requested.");
|
||||
}
|
||||
}
|
||||
}
|
27
tests/SampleTool/Program.cs
Normal file
27
tests/SampleTool/Program.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Autofac;
|
||||
|
||||
using MfGames.ToolBuilder;
|
||||
|
||||
namespace SampleTool
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
return await ToolBuilder
|
||||
.Create(
|
||||
"Sample Application",
|
||||
"SampleApplication",
|
||||
args)
|
||||
.ConfigureContainer(ConfigureContainer)
|
||||
.RunAsync();
|
||||
}
|
||||
|
||||
private static void ConfigureContainer(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterModule<SampleToolModule>();
|
||||
}
|
||||
}
|
||||
}
|
16
tests/SampleTool/SampleTool.csproj
Normal file
16
tests/SampleTool/SampleTool.csproj
Normal file
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MfGames.ToolBuilder\MfGames.ToolBuilder.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
19
tests/SampleTool/SampleToolModule.cs
Normal file
19
tests/SampleTool/SampleToolModule.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using Autofac;
|
||||
|
||||
namespace MfGames.ToolBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The Autofac module to pull in the components inside this assembly.
|
||||
/// </summary>
|
||||
public class SampleToolModule : Module
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder
|
||||
.RegisterAssemblyTypes(this.GetType().Assembly)
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces();
|
||||
}
|
||||
}
|
||||
}
|
56
tests/SampleTool/TableTopCommand.cs
Normal file
56
tests/SampleTool/TableTopCommand.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.IO;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using MfGames.ToolBuilder;
|
||||
using MfGames.ToolBuilder.Tables;
|
||||
|
||||
namespace SampleTool
|
||||
{
|
||||
public class TableTopCommand : Command, ITopCommand, ICommandHandler
|
||||
{
|
||||
private readonly DataTable table;
|
||||
|
||||
private readonly TableToolService tableService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TableTopCommand(TableToolService.Factory tableService)
|
||||
: base("table", "Display a table.")
|
||||
{
|
||||
// Create the table structure.
|
||||
this.table = new DataTable();
|
||||
this.table.Columns.Add("DefaultString", typeof(string));
|
||||
this.table.Columns.Add("DefaultInt32", typeof(int));
|
||||
this.table.Columns.Add("HiddenString", typeof(string));
|
||||
|
||||
// Create the table service for formatting and displaying results.
|
||||
this.tableService = tableService(
|
||||
this,
|
||||
this.table,
|
||||
new List<string>
|
||||
{
|
||||
"DefaultString",
|
||||
"DefaultInt32",
|
||||
});
|
||||
|
||||
// This class handles the command.
|
||||
this.Handler = this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> InvokeAsync(InvocationContext context)
|
||||
{
|
||||
this.table.Rows.Add("Row 1", 1, "Hidden 1");
|
||||
this.table.Rows.Add("Row 2", 10, "Hidden 2");
|
||||
this.table.Rows.Add("Row 3", 100, "Hidden 3");
|
||||
|
||||
this.tableService.Write(context);
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue