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