├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── Documentation └── Images │ ├── command_flow_diagram.png │ ├── command_tree_diagram.png │ ├── notes.txt │ └── process_flow.epgz ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── ROADMAP.md ├── RunInfoBuilder.sln ├── Samples └── GitCloneSample │ ├── Commands │ ├── Add.cs │ ├── Branch.cs │ ├── Checkout.cs │ ├── Commit.cs │ ├── Diff.cs │ └── Init.cs │ ├── GitCloneSample.csproj │ └── Program.cs ├── Source ├── AssemblyInfo.cs ├── Configuration │ ├── Arguments │ │ ├── ArgumentBase.cs │ │ ├── CustomArgument.cs │ │ ├── PropertyArgument.cs │ │ ├── SequenceArgument.cs │ │ └── SetArgument.cs │ ├── CommandStore.cs │ ├── Commands │ │ ├── Command.cs │ │ ├── CommandBase.cs │ │ ├── DefaultCommand.cs │ │ ├── StackableCommand.cs │ │ └── SubCommand.cs │ ├── Options │ │ ├── Option.cs │ │ └── OptionBase.cs │ └── Validators │ │ ├── CommandValidator.cs │ │ ├── DefaultCommandValidator.cs │ │ └── Rules │ │ ├── ArgumentRules.cs │ │ ├── CommandRules.cs │ │ └── OptionRules.cs ├── Exceptions.cs ├── ExtensionMethods.cs ├── Help │ ├── HelpBuilder.cs │ ├── HelpManager.cs │ └── HelpTokenResolver.cs ├── Hooks │ ├── BuildHooks.cs │ └── ReturnsWith.cs ├── Parser │ ├── ArgumentParser.cs │ └── ArgumentParserExtensions.cs ├── Processor │ ├── Functions │ │ ├── OptionFunctions.cs │ │ ├── ProgramArgumentFunctions.cs │ │ └── StageFunctions.cs │ ├── Models │ │ ├── CustomHandlerContext.cs │ │ ├── OptionProcessInfo.cs │ │ ├── OptionType.cs │ │ ├── ProcessContext.cs │ │ └── ProcessStageResult.cs │ ├── OptionSetterFactory.cs │ ├── OptionTokenizer.cs │ ├── Pipeline.cs │ ├── ReflectionHelper.cs │ ├── Stages │ │ ├── CommandStage.cs │ │ ├── CustomArgumentStage.cs │ │ ├── DefaultCommandStage.cs │ │ ├── EndProcessStage.cs │ │ ├── OptionStage.cs │ │ ├── PropertyArgumentStage.cs │ │ ├── SequenceArgumentStage.cs │ │ ├── SetArgumentStage.cs │ │ ├── Stage.cs │ │ └── SubCommandStage.cs │ └── StagesFactory.cs ├── RunInfoBuilder.cs ├── RunInfoBuilder.csproj ├── RunInfoBuilder.nuspec └── Version │ └── VersionManager.cs ├── Test ├── FunctionalTests │ ├── FunctionalTests.csproj │ ├── Models │ │ ├── TestEnums.cs │ │ ├── TestException.cs │ │ └── TestRunInfo.cs │ ├── Tests │ │ ├── ComplexScenarios │ │ │ ├── ComplexScenarioBuilderFactory.cs │ │ │ └── ComplexScenarioTests.cs │ │ ├── Help │ │ │ └── HelpManagerTests.cs │ │ ├── Hooks │ │ │ ├── NullOrEmptyReturnsTests.cs │ │ │ └── OnStartTests.cs │ │ ├── Parser │ │ │ └── NullableEnumParseTests.cs │ │ ├── Processing │ │ │ ├── Command │ │ │ │ ├── CommandTests.cs │ │ │ │ ├── DefaultCommandTests.cs │ │ │ │ └── SubCommandTests.cs │ │ │ ├── CustomArgument │ │ │ │ ├── FailTests.cs │ │ │ │ └── SuccessTests.cs │ │ │ ├── GlobalOption │ │ │ │ ├── FailTests.cs │ │ │ │ └── SuccessTests.cs │ │ │ ├── Option │ │ │ │ ├── FailTests.cs │ │ │ │ └── SuccessTests.cs │ │ │ ├── PropertyArgument │ │ │ │ ├── FailTests.cs │ │ │ │ └── SuccessTests.cs │ │ │ ├── SequenceArgument │ │ │ │ ├── FailTests.cs │ │ │ │ └── SuccessTests.cs │ │ │ └── SetArgument │ │ │ │ ├── FailTests.cs │ │ │ │ └── SuccessTests.cs │ │ └── Validations │ │ │ ├── ArgumentValidationTests.cs │ │ │ ├── CommandStoreTests.cs │ │ │ ├── CommandValidationTests.cs │ │ │ ├── GlobalOptionValidationTests.cs │ │ │ └── OptionValidationTests.cs │ └── readme.md └── UnitTests │ ├── Models │ ├── TestEnums.cs │ └── TestRunInfo.cs │ ├── Tests │ ├── Parser │ │ └── EnumParsingTests.cs │ └── Processor │ │ └── OptionTokenizerTests.cs │ └── UnitTests.csproj └── appveyor.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | ServiceFabricBackup/ 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | *.ndf 246 | 247 | # Business Intelligence projects 248 | *.rdl.data 249 | *.bim.layout 250 | *.bim_*.settings 251 | *.rptproj.rsuser 252 | 253 | # Microsoft Fakes 254 | FakesAssemblies/ 255 | 256 | # GhostDoc plugin setting file 257 | *.GhostDoc.xml 258 | 259 | # Node.js Tools for Visual Studio 260 | .ntvs_analysis.dat 261 | node_modules/ 262 | 263 | # Visual Studio 6 build log 264 | *.plg 265 | 266 | # Visual Studio 6 workspace options file 267 | *.opt 268 | 269 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 270 | *.vbw 271 | 272 | # Visual Studio LightSwitch build output 273 | **/*.HTMLClient/GeneratedArtifacts 274 | **/*.DesktopClient/GeneratedArtifacts 275 | **/*.DesktopClient/ModelManifest.xml 276 | **/*.Server/GeneratedArtifacts 277 | **/*.Server/ModelManifest.xml 278 | _Pvt_Extensions 279 | 280 | # Paket dependency manager 281 | .paket/paket.exe 282 | paket-files/ 283 | 284 | # FAKE - F# Make 285 | .fake/ 286 | 287 | # JetBrains Rider 288 | .idea/ 289 | *.sln.iml 290 | 291 | # CodeRush 292 | .cr/ 293 | 294 | # Python Tools for Visual Studio (PTVS) 295 | __pycache__/ 296 | *.pyc 297 | 298 | # Cake - Uncomment if you are using it 299 | # tools/** 300 | # !tools/packages.config 301 | 302 | # Tabs Studio 303 | *.tss 304 | 305 | # Telerik's JustMock configuration file 306 | *.jmconfig 307 | 308 | # BizTalk build output 309 | *.btp.cs 310 | *.btm.cs 311 | *.odx.cs 312 | *.xsd.cs 313 | 314 | # OpenCover UI analysis results 315 | OpenCover/ 316 | 317 | # Azure Stream Analytics local run output 318 | ASALocalRun/ 319 | 320 | # MSBuild Binary and Structured Log 321 | *.binlog 322 | 323 | # NVidia Nsight GPU debugger configuration file 324 | *.nvuser 325 | 326 | **/appsettings.json -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for considering contributing to the _RunInfoBuilder_ project. 2 | 3 | Before contributing, please carefully read the guidelines below to get an idea of the expectations for this project. 4 | 5 | - Feel free to try to tackle a reported issue. Unless it's the simplest of fixes, I'd prefer that we discuss the strategy/fix (on that issue's thread) first. The last thing I want is for someone to put in a lot of work, only to have to re-write a bunch of it. 6 | - Pull requests for brand new features won't be accepted, unless it's something that's been planned and in the roadmap. You're always welcome to fork the project to add your own features that's not supported here. 7 | 8 | ### Submitting Issues/Bugs 9 | 10 | Simply create a new issue if there's a problem found. Please spent a minute poking around to make sure the same issue doesn't already exist. 11 | 12 | For the best chance of a timely response and resolution, please be as descriptive as possible. I recommend including the following key pieces of information: 13 | 14 | 1. Repro steps - detailed steps to reproduce the issue you're having. 15 | 2. Configurations - include copies of your command configurations so we can debug it locally on our machines. 16 | 3. Stack Trace 17 | 4. Environment information - operating system, processor architecture, etc. 18 | 19 | ### Suggesting New Features / Roadmap 20 | 21 | I'm always open to new ideas or enhancements to the library. If you have any suggestions, feel free to open a new issue requesting it. Be as detailed as possible about what it is, and also explain your actual use cases that it would solve. 22 | 23 | New features ideas that are accepted will eventually be placed onto the roadmap, which you can check out in the ROADMAP.md file. It'll only be placed there once all the details have been figured out so it can be documented there. 24 | 25 | ### Branch Strategy & Pull Requests 26 | 27 | In general, there will usually only be 2 live branches: `Master` and `Development`. 28 | 29 | Once a version is released, all work afterwards will be done on the `Development` branch, so be sure to branch from there. The `Development` branch will be merged into `Master` only when a new version of the library is ready for release. 30 | 31 | All pull requests should target the `Development` branch. 32 | 33 | ### Testing 34 | 35 | Ensure tests all pass before putting your pull request up for review. 36 | 37 | Fix any broken tests, and if you added new features, be sure to add new tests that cover them. Functional tests are what I'm primarily looking for (in the `FunctionalTests` project) to ensure everything works from end-to-end. 38 | 39 | Be sure to checkout the tests that already exist to get a feel for how they should be written. Also, for tests relating to commands, be sure to write tests for both a single-level command, and multi-level commands (nested subcommands). -------------------------------------------------------------------------------- /Documentation/Images/command_flow_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushfive/RunInfoBuilder/8d0ea7afdcf085e1aefd65aee25ae1282b34093d/Documentation/Images/command_flow_diagram.png -------------------------------------------------------------------------------- /Documentation/Images/command_tree_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushfive/RunInfoBuilder/8d0ea7afdcf085e1aefd65aee25ae1282b34093d/Documentation/Images/command_tree_diagram.png -------------------------------------------------------------------------------- /Documentation/Images/notes.txt: -------------------------------------------------------------------------------- 1 | the diagrams are created using Pencil -------------------------------------------------------------------------------- /Documentation/Images/process_flow.epgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushfive/RunInfoBuilder/8d0ea7afdcf085e1aefd65aee25ae1282b34093d/Documentation/Images/process_flow.epgz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Isaiah Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 1.0.1 2 | - Add parser support for nullable enum types -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushfive/RunInfoBuilder/8d0ea7afdcf085e1aefd65aee25ae1282b34093d/ROADMAP.md -------------------------------------------------------------------------------- /RunInfoBuilder.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{BAF7BA48-E06C-4027-BA4E-66ED9E959E04}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | .gitignore = .gitignore 10 | appveyor.yml = appveyor.yml 11 | CONTRIBUTING.md = CONTRIBUTING.md 12 | LICENSE = LICENSE 13 | README.md = README.md 14 | RELEASE_NOTES.md = RELEASE_NOTES.md 15 | ROADMAP.md = ROADMAP.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{8AF17C29-9FD3-4C6C-82C2-693519E7FF81}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "Test\UnitTests\UnitTests.csproj", "{BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunctionalTests", "Test\FunctionalTests\FunctionalTests.csproj", "{57981916-3F35-4E99-9CAA-7E09E70C6476}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{499B72C5-DFC4-4356-8FD0-6E741E23831B}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitCloneSample", "Samples\GitCloneSample\GitCloneSample.csproj", "{F1A7141C-3B1C-428D-BDE9-435DDA003B78}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RunInfoBuilder", "Source\RunInfoBuilder.csproj", "{24D7D44B-658B-48BA-A184-02D4D070907E}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | Test|Any CPU = Test|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF}.Test|Any CPU.ActiveCfg = Test|Any CPU 41 | {BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF}.Test|Any CPU.Build.0 = Test|Any CPU 42 | {57981916-3F35-4E99-9CAA-7E09E70C6476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {57981916-3F35-4E99-9CAA-7E09E70C6476}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {57981916-3F35-4E99-9CAA-7E09E70C6476}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {57981916-3F35-4E99-9CAA-7E09E70C6476}.Test|Any CPU.ActiveCfg = Test|Any CPU 46 | {57981916-3F35-4E99-9CAA-7E09E70C6476}.Test|Any CPU.Build.0 = Test|Any CPU 47 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78}.Test|Any CPU.ActiveCfg = Debug|Any CPU 52 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78}.Test|Any CPU.Build.0 = Debug|Any CPU 53 | {24D7D44B-658B-48BA-A184-02D4D070907E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {24D7D44B-658B-48BA-A184-02D4D070907E}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {24D7D44B-658B-48BA-A184-02D4D070907E}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {24D7D44B-658B-48BA-A184-02D4D070907E}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {24D7D44B-658B-48BA-A184-02D4D070907E}.Test|Any CPU.ActiveCfg = Debug|Any CPU 58 | {24D7D44B-658B-48BA-A184-02D4D070907E}.Test|Any CPU.Build.0 = Debug|Any CPU 59 | EndGlobalSection 60 | GlobalSection(SolutionProperties) = preSolution 61 | HideSolutionNode = FALSE 62 | EndGlobalSection 63 | GlobalSection(NestedProjects) = preSolution 64 | {BF82D217-74FB-4DE7-B8C6-9079C1E0B9AF} = {8AF17C29-9FD3-4C6C-82C2-693519E7FF81} 65 | {57981916-3F35-4E99-9CAA-7E09E70C6476} = {8AF17C29-9FD3-4C6C-82C2-693519E7FF81} 66 | {F1A7141C-3B1C-428D-BDE9-435DDA003B78} = {499B72C5-DFC4-4356-8FD0-6E741E23831B} 67 | {24D7D44B-658B-48BA-A184-02D4D070907E} = {BAF7BA48-E06C-4027-BA4E-66ED9E959E04} 68 | EndGlobalSection 69 | GlobalSection(ExtensibilityGlobals) = postSolution 70 | SolutionGuid = {FA5A825D-533A-4664-9D74-F3C20D3FF7E1} 71 | EndGlobalSection 72 | EndGlobal 73 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Commands/Add.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Samples.HelpExamples.Commands 6 | { 7 | public class AddRunInfo 8 | { 9 | // Options 10 | public bool DryRun { get; set; } 11 | public bool Verbose { get; set; } 12 | public bool IgnoreErrors { get; set; } 13 | public bool Refresh { get; set; } 14 | } 15 | 16 | public class Add 17 | { 18 | public static Command Configuration => 19 | new Command 20 | { 21 | Key = "add", 22 | Description = "Add file contents to the index.", 23 | Options = 24 | { 25 | new Option 26 | { 27 | Key = "dry-run | n", 28 | Property = ri => ri.DryRun 29 | }, 30 | new Option 31 | { 32 | Key = "verbose | v", 33 | Property = ri => ri.Verbose 34 | }, 35 | new Option 36 | { 37 | Key = "ignore-errors", 38 | Property = ri => ri.IgnoreErrors 39 | }, 40 | new Option 41 | { 42 | Key = "refresh", 43 | Property = ri => ri.Refresh 44 | } 45 | } 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Commands/Branch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Samples.HelpExamples.Commands 6 | { 7 | public class BranchRunInfo 8 | { 9 | // Arguments 10 | public string BranchName { get; set; } 11 | 12 | // Options 13 | public bool Delete { get; set; } 14 | public bool Force { get; set; } 15 | public bool IgnoreCase { get; set; } 16 | } 17 | 18 | public class Branch 19 | { 20 | public static Command Configuration => 21 | new Command 22 | { 23 | Key = "branch", 24 | Description = "List, create, or delete branches.", 25 | Arguments = 26 | { 27 | new PropertyArgument 28 | { 29 | HelpToken = "", 30 | Property = ri => ri.BranchName 31 | } 32 | }, 33 | Options = 34 | { 35 | new Option 36 | { 37 | Key = "delete | d", 38 | Property = ri => ri.Delete 39 | }, 40 | new Option 41 | { 42 | Key = "force | f", 43 | Property = ri => ri.Force 44 | }, 45 | new Option 46 | { 47 | Key = "ignore-case | i", 48 | Property = ri => ri.IgnoreCase 49 | } 50 | } 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Commands/Checkout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Samples.HelpExamples.Commands 6 | { 7 | public class CheckoutRunInfo 8 | { 9 | // Arguments 10 | public string Branch { get; set; } 11 | 12 | // Options 13 | public bool Quiet { get; set; } 14 | public bool Force { get; set; } 15 | public bool Track { get; set; } 16 | } 17 | 18 | public class Checkout 19 | { 20 | public static Command Configuration => 21 | new Command 22 | { 23 | Key = "checkout", 24 | Description = "Switch branches or restore working tree files.", 25 | Arguments = 26 | { 27 | new PropertyArgument 28 | { 29 | HelpToken = "", 30 | Property = ri => ri.Branch 31 | } 32 | }, 33 | Options = 34 | { 35 | new Option 36 | { 37 | Key = "quiet | q", 38 | Property = ri => ri.Quiet 39 | }, 40 | new Option 41 | { 42 | Key = "force | f", 43 | Property = ri => ri.Force 44 | }, 45 | new Option 46 | { 47 | Key = "track | t", 48 | Property = ri => ri.Track 49 | } 50 | } 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Commands/Commit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Samples.HelpExamples.Commands 6 | { 7 | public class CommitRunInfo 8 | { 9 | // Options 10 | public bool All { get; set; } 11 | public bool Patch { get; set; } 12 | public bool Branch { get; set; } 13 | } 14 | 15 | public class Commit 16 | { 17 | public static Command Configuration => 18 | new Command 19 | { 20 | Key = "commit", 21 | Description = "Stores the current contents of the index in a new commit along with a log message from the user describing the changes.", 22 | Options = 23 | { 24 | new Option 25 | { 26 | Key = "all | a", 27 | Property = ri => ri.All 28 | }, 29 | new Option 30 | { 31 | Key = "patch | p", 32 | Property = ri => ri.Patch 33 | }, 34 | new Option 35 | { 36 | Key = "branch", 37 | Property = ri => ri.Branch 38 | } 39 | } 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Commands/Diff.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.Samples.HelpExamples.Commands 7 | { 8 | public class DiffRunInfo 9 | { 10 | // Arguments 11 | public string FirstBranch { get; set; } 12 | public string SecondBranch { get; set; } 13 | 14 | // Options 15 | public bool NoPatch { get; set; } 16 | public bool Raw { get; set; } 17 | public bool Minimal { get; set; } 18 | } 19 | 20 | public class Diff 21 | { 22 | public static Command Configuration => 23 | new Command 24 | { 25 | Key = "diff", 26 | Description = "Switch branches or restore working tree files.", 27 | Arguments = 28 | { 29 | new CustomArgument 30 | { 31 | HelpToken = "[first-branch]...[second-branch]", 32 | Count = 1, 33 | Handler = context => 34 | { 35 | string[] branchSplit = context.ProgramArguments.Single().Split("..."); 36 | context.RunInfo.FirstBranch = branchSplit[0]; 37 | context.RunInfo.SecondBranch = branchSplit[1]; 38 | return ProcessResult.Continue; 39 | } 40 | } 41 | }, 42 | Options = 43 | { 44 | new Option 45 | { 46 | Key = "no-patch | s", 47 | Property = ri => ri.NoPatch 48 | }, 49 | new Option 50 | { 51 | Key = "raw", 52 | Property = ri => ri.Raw 53 | }, 54 | new Option 55 | { 56 | Key = "minimal", 57 | Property = ri => ri.Minimal 58 | } 59 | } 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Commands/Init.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Samples.HelpExamples.Commands 6 | { 7 | public class InitRunInfo 8 | { 9 | // Options 10 | public bool Quiet { get; set; } 11 | public bool Bare { get; set; } 12 | } 13 | 14 | public class Init 15 | { 16 | public static Command Configuration => 17 | new Command 18 | { 19 | Key = "init", 20 | Description = "Create an empty Git repository or reinitialize an existing one.", 21 | Options = 22 | { 23 | new Option 24 | { 25 | Key = "quiet | q", 26 | Property = ri => ri.Quiet 27 | }, 28 | new Option 29 | { 30 | Key = "bare", 31 | Property = ri => ri.Bare 32 | } 33 | } 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/GitCloneSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | R5.RunInfoBuilder.Samples.HelpExamples 7 | R5.RunInfoBuilder.Samples.HelpExamples 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Samples/GitCloneSample/Program.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Samples.HelpExamples.Commands; 2 | using System; 3 | 4 | namespace R5.RunInfoBuilder.Samples.HelpExamples 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | var builder = new RunInfoBuilder(); 11 | 12 | builder.Version.Set("2.19.1"); 13 | 14 | builder.Help 15 | .SetProgramName("git") 16 | .InvokeOnBuildFail(suppressException: false); 17 | 18 | ConfigureCommands(builder); 19 | 20 | builder.Build(new string[] { "--help" }); 21 | Console.ReadKey(); 22 | } 23 | 24 | private static void ConfigureCommands(RunInfoBuilder builder) 25 | { 26 | builder.Commands 27 | .Add(Add.Configuration) 28 | .Add(Branch.Configuration) 29 | .Add(Checkout.Configuration) 30 | .Add(Commit.Configuration) 31 | .Add(Diff.Configuration) 32 | .Add(Init.Configuration); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: InternalsVisibleTo("R5.RunInfoBuilder.UnitTests")] 5 | [assembly: CLSCompliant(true)] -------------------------------------------------------------------------------- /Source/Configuration/Arguments/ArgumentBase.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration; 2 | using R5.RunInfoBuilder.Processor.Stages; 3 | 4 | namespace R5.RunInfoBuilder 5 | { 6 | /// 7 | /// The base abstract class Arguments derive from. 8 | /// 9 | /// The RunInfo type the argument's associated to. 10 | public abstract class ArgumentBase 11 | where TRunInfo : class 12 | { 13 | /// 14 | /// The token displayed in the help menu that represents this argument. 15 | /// 16 | public string HelpToken { get; set; } 17 | 18 | internal abstract Stage ToStage(); 19 | 20 | internal abstract string GetHelpToken(); 21 | 22 | internal abstract void Validate(int commandLevel); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Configuration/Arguments/CustomArgument.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 2 | using R5.RunInfoBuilder.Processor.Stages; 3 | using System; 4 | 5 | namespace R5.RunInfoBuilder 6 | { 7 | /// 8 | /// Handles n number of consecutive program arguments using a custom callback. 9 | /// 10 | /// The RunInfo type the argument's associated to. 11 | public class CustomArgument : ArgumentBase 12 | where TRunInfo : class 13 | { 14 | /// 15 | /// The number of program arguments handled. 16 | /// 17 | public int Count { get; set; } 18 | 19 | /// 20 | /// The custom callback that handles the program arguments. 21 | /// 22 | public Func, ProcessStageResult> Handler { get; set; } 23 | 24 | internal override Stage ToStage() 25 | { 26 | return new CustomArgumentStage(Count, Handler); 27 | } 28 | 29 | internal override string GetHelpToken() 30 | { 31 | return string.IsNullOrWhiteSpace(HelpToken) ? "" : HelpToken; 32 | } 33 | 34 | internal override void Validate(int commandLevel) 35 | { 36 | ArgumentRules.Custom.CountMustBeGreaterThanZero(this, commandLevel); 37 | ArgumentRules.Custom.HandlerMustBeSet(this, commandLevel); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Source/Configuration/Arguments/PropertyArgument.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 2 | using R5.RunInfoBuilder.Help; 3 | using R5.RunInfoBuilder.Processor.Stages; 4 | using System; 5 | using System.Linq.Expressions; 6 | 7 | namespace R5.RunInfoBuilder 8 | { 9 | /// 10 | /// Defines a 1-to-1 mapping of a program argument to a property on the RunInfo. 11 | /// 12 | /// The RunInfo type the argument's associated to. 13 | public class PropertyArgument : ArgumentBase 14 | where TRunInfo : class 15 | { 16 | /// 17 | /// An expression of the RunInfo property the value will be bound to. 18 | /// 19 | public Expression> Property { get; set; } 20 | 21 | /// 22 | /// An optional callback that's invoked after the program argument is successfully parsed. 23 | /// 24 | /// 25 | /// This is invoked before the parsed value is bound to the RunInfo property. 26 | /// 27 | public Func OnParsed { get; set; } 28 | 29 | /// 30 | /// An optional function used to generate the error message on parsing error. 31 | /// 32 | /// 33 | /// The single argument to the Func is the program argument that failed to parse. 34 | /// 35 | public Func OnParseErrorUseMessage { get; set; } 36 | 37 | internal override Stage ToStage() 38 | { 39 | return new PropertyArgumentStage(Property, OnParsed, OnParseErrorUseMessage); 40 | } 41 | 42 | internal override string GetHelpToken() 43 | { 44 | if (!string.IsNullOrWhiteSpace(HelpToken)) 45 | { 46 | return HelpToken; 47 | } 48 | 49 | return HelpTokenResolver.ForPropertyArgument(); 50 | } 51 | 52 | internal override void Validate(int commandLevel) 53 | { 54 | ArgumentRules.Property.PropertyMappingIsSet(this, commandLevel); 55 | ArgumentRules.Property.MappedPropertyIsWritable(this, commandLevel); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/Configuration/Arguments/SequenceArgument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 5 | using R5.RunInfoBuilder.Help; 6 | using R5.RunInfoBuilder.Processor.Stages; 7 | 8 | namespace R5.RunInfoBuilder 9 | { 10 | /// 11 | /// Parses and adds program arguments into a specified list on the RunInfo. 12 | /// 13 | /// 14 | /// Continues to consider program arguments until it hits an option, subcommand, or runs out. 15 | /// 16 | /// The RunInfo type the argument's associated to. 17 | /// The Type of the list the parsed sequence of argument values are added to. 18 | public class SequenceArgument : ArgumentBase 19 | where TRunInfo : class 20 | { 21 | /// 22 | /// An expression of the RunInfo list property the parsed values will be added to. 23 | /// 24 | public Expression>> ListProperty { get; set; } 25 | 26 | /// 27 | /// An optional callback that's invoked after each program argument is successfully parsed. 28 | /// 29 | /// 30 | /// This is invoked before the parsed value is added to the list. 31 | /// 32 | public Func OnParsed { get; set; } 33 | 34 | /// 35 | /// An optional function used to generate the error message on parsing error. 36 | /// 37 | /// 38 | /// The single argument to the Func is the program argument that failed to parse. 39 | /// 40 | public Func OnParseErrorUseMessage { get; set; } 41 | 42 | internal override Stage ToStage() 43 | { 44 | return new SequenceArgumentStage(ListProperty, OnParsed, OnParseErrorUseMessage); 45 | } 46 | 47 | internal override string GetHelpToken() 48 | { 49 | if (!string.IsNullOrWhiteSpace(HelpToken)) 50 | { 51 | return HelpToken; 52 | } 53 | 54 | return HelpTokenResolver.ForSequenceArgument(); 55 | } 56 | 57 | internal override void Validate(int commandLevel) 58 | { 59 | ArgumentRules.Sequence.MappedPropertyMustBeSet(this, commandLevel); 60 | ArgumentRules.Sequence.MappedPropertyIsWritable(this, commandLevel); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Source/Configuration/Arguments/SetArgument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 6 | using R5.RunInfoBuilder.Processor.Stages; 7 | 8 | namespace R5.RunInfoBuilder 9 | { 10 | /// 11 | /// Requires the program argument to be from the specified set. 12 | /// 13 | /// /// The RunInfo type the argument's associated to. 14 | /// The Type of the property the parsed argument value binds to. 15 | public class SetArgument : ArgumentBase 16 | where TRunInfo : class 17 | { 18 | /// 19 | /// An expression of the RunInfo property the value will be bound to. 20 | /// 21 | public Expression> Property { get; set; } 22 | 23 | /// 24 | /// List of tuples containing the pairs of keys and values for the set. 25 | /// 26 | public List<(string Label, TProperty Value)> Values { get; set; } 27 | = new List<(string, TProperty)>(); 28 | 29 | internal override string GetHelpToken() 30 | { 31 | string result = "<"; 32 | result += string.Join("|", Values.Select(v => v.Label)); 33 | return result + ">"; 34 | } 35 | 36 | internal override Stage ToStage() 37 | { 38 | return new SetArgumentStage(Property, Values); 39 | } 40 | 41 | internal override void Validate(int commandLevel) 42 | { 43 | ArgumentRules.Set.MappedPropertyMustBeSet(this, commandLevel); 44 | ArgumentRules.Set.MappedPropertyIsWritable(this, commandLevel); 45 | ArgumentRules.Set.ValuesMustBeSet(this, commandLevel); 46 | ArgumentRules.Set.ValuesMustContainAtLeastTwoItems(this, commandLevel); 47 | ArgumentRules.Set.ValueLabelsMustBeUnique(this, commandLevel); 48 | ArgumentRules.Set.ValueValuesMustBeUnique(this, commandLevel); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Source/Configuration/CommandStore.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration.Validators; 2 | using R5.RunInfoBuilder.Parser; 3 | using R5.RunInfoBuilder.Processor; 4 | using R5.RunInfoBuilder.Processor.Stages; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace R5.RunInfoBuilder 11 | { 12 | /// 13 | /// Provides methods to store the command configurations. 14 | /// 15 | public class CommandStore 16 | { 17 | internal const string DefaultKey = "__DEFAULT__"; 18 | 19 | private StagesFactory _stagesFactory { get; } 20 | private ArgumentParser _parser { get; } 21 | private HelpManager _helpManager { get; } 22 | 23 | // Key: Command key (or DefaultKey) 24 | // Value: Func> (pass args[] to get the corresponding pipeline) 25 | private Dictionary _pipelineFactoryMap { get; } 26 | 27 | // Key: Command key (or DefaultKey) 28 | // Value: CommandBase 29 | private Dictionary _commandMap { get; } 30 | internal bool AreConfigured => _commandMap.Any(); 31 | 32 | internal CommandStore( 33 | ArgumentParser parser, 34 | HelpManager helpManager) 35 | { 36 | _stagesFactory = new StagesFactory(); 37 | _parser = parser; 38 | _helpManager = helpManager; 39 | 40 | _pipelineFactoryMap = new Dictionary(); 41 | _commandMap = new Dictionary(); 42 | } 43 | 44 | /// 45 | /// Adds a normal command (specified by a unique key). 46 | /// 47 | /// The run info Type this command is mapped to. 48 | /// The command configuration object. 49 | /// An optional callback that's invoked with the built TRunInfo run info object. 50 | /// The CommandStore instance. 51 | public CommandStore Add(Command command, 52 | Action postBuildCallback = null) 53 | where TRunInfo : class 54 | { 55 | if (command == null) 56 | { 57 | throw new CommandValidationException( 58 | "Command must be provided.", 59 | CommandValidationError.NullObject, commandLevel: 0); 60 | } 61 | 62 | if (string.IsNullOrWhiteSpace(command.Key)) 63 | { 64 | throw new CommandValidationException( 65 | "Command key must be provided.", 66 | CommandValidationError.KeyNotProvided, commandLevel: 0); 67 | } 68 | 69 | if (IsCommand(command.Key)) 70 | { 71 | throw new CommandValidationException( 72 | $"Command with key '{command.Key} has already been configured.", 73 | CommandValidationError.DuplicateKey, commandLevel: 0); 74 | } 75 | 76 | CommandValidator.Validate(command); 77 | 78 | Func> pipelineFactory = args => 79 | { 80 | Queue> stages = _stagesFactory.Create( 81 | command, command.GlobalOptions.Any(), postBuildCallback); 82 | 83 | // skip the first arg (command key) 84 | args = args.Skip(1).ToArray(); 85 | 86 | return new Pipeline(stages, args, command, _parser, command.GlobalOptions); 87 | }; 88 | 89 | _pipelineFactoryMap.Add(command.Key, pipelineFactory); 90 | 91 | _helpManager.ConfigureForCommand(command); 92 | 93 | return this; 94 | } 95 | 96 | /// 97 | /// Adds the default command (implied if program arguments don't begin with a configured command key). 98 | /// 99 | /// The run info Type this command is mapped to. 100 | /// The default command configuration object. 101 | /// An optional callback that's invoked with the built TRunInfo run info object. 102 | /// The CommandStore instance. 103 | public CommandStore AddDefault(DefaultCommand defaultCommand, 104 | Action postBuildCallback = null) 105 | where TRunInfo : class 106 | { 107 | if (defaultCommand == null) 108 | { 109 | throw new CommandValidationException( 110 | "Command must be provided.", 111 | CommandValidationError.NullObject, commandLevel: -1); 112 | } 113 | 114 | if (IsCommand(CommandStore.DefaultKey)) 115 | { 116 | throw new CommandValidationException( 117 | "Default command has already been configured.", 118 | CommandValidationError.DuplicateKey, commandLevel: -1); 119 | } 120 | 121 | DefaultCommandValidator.Validate(defaultCommand); 122 | 123 | Func> pipelineFactory = args => 124 | { 125 | Queue> stages = _stagesFactory.Create(defaultCommand, postBuildCallback); 126 | return new Pipeline(stages, args, defaultCommand, _parser, globalOptions: null); 127 | }; 128 | 129 | _pipelineFactoryMap.Add(CommandStore.DefaultKey, pipelineFactory); 130 | 131 | _helpManager.ConfigureForDefaultCommand(defaultCommand); 132 | 133 | return this; 134 | } 135 | 136 | internal object ResolvePipelineFromArgs(string[] args) 137 | { 138 | if (args.Length == 0 || !IsCommand(args[0])) 139 | { 140 | if (!_pipelineFactoryMap.ContainsKey(CommandStore.DefaultKey)) 141 | { 142 | throw new ProcessException("A DefaultCommand is not configured and a Command key wasn't matched."); 143 | } 144 | 145 | dynamic defaultFactory = _pipelineFactoryMap[CommandStore.DefaultKey]; 146 | return defaultFactory.Invoke(args); 147 | } 148 | 149 | if (!_pipelineFactoryMap.ContainsKey(args[0])) 150 | { 151 | throw new ProcessException($"Failed to process command '{args[0]}', its pipeline could not be found."); 152 | } 153 | 154 | dynamic factory = _pipelineFactoryMap[args[0]]; 155 | 156 | return factory.Invoke(args); 157 | } 158 | 159 | internal bool IsCommand(string key) => _pipelineFactoryMap.ContainsKey(key); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /Source/Configuration/Commands/Command.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Stages; 2 | using System.Collections.Generic; 3 | 4 | namespace R5.RunInfoBuilder 5 | { 6 | /// 7 | /// The command configuration object. All other configurations like arguments start from here. 8 | /// 9 | /// The RunInfo type that's built from the command. 10 | public class Command : StackableCommand 11 | where TRunInfo : class 12 | { 13 | /// 14 | /// List of optional global Options associated to the command. 15 | /// These are scoped to be accessible to any SubCommand in the tree. 16 | /// 17 | public List> GlobalOptions { get; set; } = new List>(); 18 | 19 | internal Stage ToStage() 20 | { 21 | return new CommandStage(OnMatched); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Configuration/Commands/CommandBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace R5.RunInfoBuilder 5 | { 6 | /// 7 | /// The base abstract class Commands derive from. 8 | /// 9 | /// The RunInfo type the command's associated to. 10 | public abstract class CommandBase 11 | where TRunInfo : class 12 | { 13 | /// 14 | /// Description that's displayed in the help menu. 15 | /// 16 | public string Description { get; set; } 17 | 18 | /// 19 | /// List of Arguments required to run the command. 20 | /// 21 | public List> Arguments { get; set; } = new List>(); 22 | 23 | /// 24 | /// List of optional Options associated to the command. 25 | /// 26 | public List> Options { get; set; } = new List>(); 27 | 28 | /// 29 | /// An optional callback that's invoked immediately after the command is matched and begins processing. 30 | /// 31 | /// 32 | /// This is the first thing processed in a command (eg before arguments, options, etc). 33 | /// 34 | public Func OnMatched { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Source/Configuration/Commands/DefaultCommand.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Stages; 2 | 3 | namespace R5.RunInfoBuilder 4 | { 5 | /// 6 | /// The configuration for the default behavior when the program is run without specifying a command key. 7 | /// 8 | /// The RunInfo type the command's associated to. 9 | public class DefaultCommand : CommandBase 10 | where TRunInfo : class 11 | { 12 | internal Stage ToStage() 13 | { 14 | return new DefaultCommandStage(OnMatched); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Configuration/Commands/StackableCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace R5.RunInfoBuilder 4 | { 5 | /// 6 | /// The base abstract class Commands and SubCommands derive from. 7 | /// 8 | /// These Commands can be nested into a tree structure, unlike the DefaultCommand type. 9 | /// 10 | /// 11 | /// The RunInfo type the command's associated to. 12 | public abstract class StackableCommand : CommandBase 13 | where TRunInfo : class 14 | { 15 | /// 16 | /// A unique key representing the command. 17 | /// 18 | /// 19 | /// The key only needs to be unique within the same level of command. 20 | /// A command and one of its' subcommands can share the same key. 21 | /// 22 | public string Key { get; set; } 23 | 24 | /// 25 | /// List of subcommands that are associated to this command. 26 | /// 27 | public List> SubCommands { get; set; } = new List>(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Configuration/Commands/SubCommand.cs: -------------------------------------------------------------------------------- 1 | namespace R5.RunInfoBuilder 2 | { 3 | /// 4 | /// The Command type that represents any nested commands (ie not the root Command) 5 | /// 6 | /// The RunInfo type the command's associated to. 7 | public class SubCommand : StackableCommand 8 | where TRunInfo : class 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/Configuration/Options/Option.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 2 | using R5.RunInfoBuilder.Processor; 3 | using R5.RunInfoBuilder.Processor.Models; 4 | using System; 5 | using System.Linq.Expressions; 6 | 7 | namespace R5.RunInfoBuilder 8 | { 9 | /// 10 | /// The option configuration object. 11 | /// 12 | /// The RunInfo type the option binds a value to. 13 | /// The type of the RunInfo property the option binds to. 14 | public class Option : OptionBase 15 | where TRunInfo : class 16 | { 17 | /// 18 | /// The token displayed in the help menu representing the option. 19 | /// 20 | public string HelpToken { get; set; } 21 | 22 | /// 23 | /// An expression of the RunInfo property the option binds to. 24 | /// 25 | public Expression> Property { get; set; } 26 | 27 | /// 28 | /// An optional callback that's invoked after the program argument is successfully parsed. 29 | /// 30 | /// 31 | /// This is invoked before the parsed value is bound to the RunInfo property. 32 | /// 33 | public Func OnParsed { get; set; } 34 | 35 | /// 36 | /// An optional function used to generate the error message on parsing error. 37 | /// 38 | /// 39 | /// The single argument to the Func is the option value that failed to parse. 40 | /// 41 | public Func OnParseErrorUseMessage { get; set; } 42 | 43 | public Option() 44 | : base(typeof(TProperty)) { } 45 | 46 | internal override void ValidateOption(int commandLevel) 47 | { 48 | OptionRules.PropertyMappingIsSet(this, commandLevel); 49 | OptionRules.MappedPropertyIsWritable(this, commandLevel); 50 | OptionRules.OnProcessCallbackNotAllowedForBoolOptions(this, commandLevel); 51 | } 52 | 53 | internal override OptionProcessInfo GetProcessInfo() 54 | { 55 | (Action Setter, Type Type) = OptionSetterFactory.CreateSetter(this); 56 | 57 | return new OptionProcessInfo(Setter, Type, OnParsed, OnParseErrorUseMessage); 58 | } 59 | 60 | internal override string GetHelpToken() 61 | { 62 | if (!string.IsNullOrWhiteSpace(HelpToken)) 63 | { 64 | return HelpToken; 65 | } 66 | 67 | (string fullKey, char? shortKey) = OptionTokenizer.TokenizeKeyConfiguration(Key); 68 | 69 | string result = $"[--{fullKey}"; 70 | 71 | if (shortKey.HasValue) 72 | { 73 | result += $"|-{shortKey.Value}"; 74 | } 75 | 76 | return result + "]"; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Configuration/Options/OptionBase.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | 4 | namespace R5.RunInfoBuilder 5 | { 6 | public abstract class OptionBase 7 | where TRunInfo : class 8 | { 9 | public string Key { get; set; } 10 | internal Type Type { get; } 11 | 12 | protected OptionBase(Type type) 13 | { 14 | Type = type; 15 | } 16 | 17 | internal abstract OptionProcessInfo GetProcessInfo(); 18 | 19 | internal abstract string GetHelpToken(); 20 | 21 | internal abstract void ValidateOption(int commandLevel); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Source/Configuration/Validators/CommandValidator.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.Configuration.Validators 7 | { 8 | internal static class CommandValidator 9 | { 10 | internal static void Validate(Command command) 11 | where TRunInfo : class 12 | { 13 | CommandRules.Common.KeyIsNotNullOrEmpty(command, 0); 14 | 15 | if (command.Arguments != null) 16 | { 17 | CommandRules.Common.ArgumentsCannotBeNull(command, 0); 18 | 19 | foreach (ArgumentBase argument in command.Arguments) 20 | { 21 | argument.Validate(0); 22 | } 23 | } 24 | 25 | if (command.Options != null) 26 | { 27 | CommandRules.Common.OptionsCannotBeNull(command.Options, 0); 28 | CommandRules.Common.OptionKeysMustMatchRegex(command.Options, 0); 29 | CommandRules.Common.OptionKeysMustBeUnique(command.Options, command.GlobalOptions, 0); 30 | 31 | foreach (OptionBase option in command.Options) 32 | { 33 | option.ValidateOption(0); 34 | } 35 | } 36 | 37 | if (command.GlobalOptions != null) 38 | { 39 | CommandRules.Common.OptionsCannotBeNull(command.GlobalOptions, 0); 40 | CommandRules.Common.OptionKeysMustMatchRegex(command.GlobalOptions, 0); 41 | CommandRules.Common.OptionKeysMustBeUnique(command.GlobalOptions, null, 0); 42 | 43 | foreach (OptionBase option in command.GlobalOptions) 44 | { 45 | option.ValidateOption(0); 46 | } 47 | } 48 | 49 | if (command.SubCommands != null) 50 | { 51 | CommandRules.SubCommand.SubCommandsCannotBeNull(command, 0); 52 | CommandRules.SubCommand.SubCommandKeysMustBeUnique(command, 0); 53 | 54 | foreach (SubCommand subCommand in command.SubCommands) 55 | { 56 | ValidateSubCommand(subCommand, command.GlobalOptions, 1); 57 | } 58 | } 59 | } 60 | 61 | private static void ValidateSubCommand(SubCommand subCommand, 62 | List> globalOptions, int commandLevel) where TRunInfo : class 63 | { 64 | CommandRules.Common.KeyIsNotNullOrEmpty(subCommand, commandLevel); 65 | 66 | if (subCommand.Arguments != null) 67 | { 68 | CommandRules.Common.ArgumentsCannotBeNull(subCommand, commandLevel); 69 | 70 | foreach (ArgumentBase argument in subCommand.Arguments) 71 | { 72 | argument.Validate(commandLevel); 73 | } 74 | } 75 | 76 | if (subCommand.Options != null) 77 | { 78 | CommandRules.Common.OptionsCannotBeNull(subCommand.Options, commandLevel); 79 | CommandRules.Common.OptionKeysMustMatchRegex(subCommand.Options, commandLevel); 80 | CommandRules.Common.OptionKeysMustBeUnique(subCommand.Options, globalOptions, commandLevel); 81 | 82 | foreach (OptionBase option in subCommand.Options) 83 | { 84 | option.ValidateOption(commandLevel); 85 | } 86 | } 87 | 88 | if (subCommand.SubCommands != null) 89 | { 90 | CommandRules.SubCommand.SubCommandsCannotBeNull(subCommand, commandLevel); 91 | CommandRules.SubCommand.SubCommandKeysMustBeUnique(subCommand, commandLevel); 92 | 93 | foreach (SubCommand sc in subCommand.SubCommands) 94 | { 95 | ValidateSubCommand(sc, globalOptions, commandLevel + 1); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Source/Configuration/Validators/DefaultCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Configuration.Validators.Rules; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.Configuration.Validators 7 | { 8 | internal static class DefaultCommandValidator 9 | { 10 | internal static void Validate(DefaultCommand defaultCommand) 11 | where TRunInfo : class 12 | { 13 | int commandLevel = -1; 14 | 15 | if (defaultCommand.Arguments != null) 16 | { 17 | CommandRules.Common.ArgumentsCannotBeNull(defaultCommand, commandLevel); 18 | 19 | foreach (ArgumentBase argument in defaultCommand.Arguments) 20 | { 21 | argument.Validate(commandLevel); 22 | } 23 | } 24 | 25 | if (defaultCommand.Options != null) 26 | { 27 | CommandRules.Common.OptionsCannotBeNull(defaultCommand.Options, commandLevel); 28 | CommandRules.Common.OptionKeysMustMatchRegex(defaultCommand.Options, commandLevel); 29 | CommandRules.Common.OptionKeysMustBeUnique(defaultCommand.Options, null, commandLevel); 30 | 31 | foreach (OptionBase option in defaultCommand.Options) 32 | { 33 | option.ValidateOption(commandLevel); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/Configuration/Validators/Rules/ArgumentRules.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor; 2 | using System.Linq; 3 | 4 | namespace R5.RunInfoBuilder.Configuration.Validators.Rules 5 | { 6 | internal static class ArgumentRules 7 | { 8 | internal static class Property 9 | { 10 | internal static void PropertyMappingIsSet(PropertyArgument argument, 11 | int commandLevel) where TRunInfo : class 12 | { 13 | if (argument.Property == null) 14 | { 15 | throw new CommandValidationException("Property Argument is missing its property mapping expression.", 16 | CommandValidationError.NullPropertyExpression, commandLevel); 17 | } 18 | } 19 | 20 | internal static void MappedPropertyIsWritable(PropertyArgument argument, 21 | int commandLevel) where TRunInfo : class 22 | { 23 | if (!ReflectionHelper.PropertyIsWritable(argument.Property, out string propertyName)) 24 | { 25 | throw new CommandValidationException($"Property Argument's property '{propertyName}' " 26 | + "is not writable. Try adding a setter.", 27 | CommandValidationError.PropertyNotWritable, commandLevel); 28 | } 29 | } 30 | } 31 | 32 | internal static class Custom 33 | { 34 | internal static void CountMustBeGreaterThanZero(CustomArgument argument, 35 | int commandLevel) where TRunInfo : class 36 | { 37 | if (argument.Count <= 0) 38 | { 39 | throw new CommandValidationException("Custom Argument has an invalid count. Must be greater than 0.", 40 | CommandValidationError.InvalidCount, commandLevel); 41 | } 42 | } 43 | 44 | internal static void HandlerMustBeSet(CustomArgument argument, 45 | int commandLevel) where TRunInfo : class 46 | { 47 | if (argument.Handler == null) 48 | { 49 | throw new CommandValidationException("Custom Argument is missing its handler callback.", 50 | CommandValidationError.NullCustomHandler, commandLevel); 51 | } 52 | } 53 | } 54 | 55 | internal static class Sequence 56 | { 57 | internal static void MappedPropertyMustBeSet(SequenceArgument argument, 58 | int commandLevel) where TRunInfo : class 59 | { 60 | if (argument.ListProperty == null) 61 | { 62 | throw new CommandValidationException("Sequence Argument is missing its property mapping expression.", 63 | CommandValidationError.NullPropertyExpression, commandLevel); 64 | } 65 | } 66 | 67 | internal static void MappedPropertyIsWritable(SequenceArgument argument, 68 | int commandLevel) where TRunInfo : class 69 | { 70 | if (!ReflectionHelper.PropertyIsWritable(argument.ListProperty, out string propertyName)) 71 | { 72 | throw new CommandValidationException($"Sequence Argument's property '{propertyName}' " 73 | + "is not writable. Try adding a setter.", 74 | CommandValidationError.PropertyNotWritable, commandLevel); 75 | } 76 | } 77 | } 78 | 79 | internal static class Set 80 | { 81 | internal static void MappedPropertyMustBeSet(SetArgument argument, 82 | int commandLevel)where TRunInfo : class 83 | { 84 | if (argument.Property == null) 85 | { 86 | throw new CommandValidationException("Set Argument is missing its property mapping expression.", 87 | CommandValidationError.NullPropertyExpression, commandLevel); 88 | } 89 | } 90 | 91 | internal static void MappedPropertyIsWritable(SetArgument argument, 92 | int commandLevel)where TRunInfo : class 93 | { 94 | if (!ReflectionHelper.PropertyIsWritable(argument.Property, out string propertyName)) 95 | { 96 | throw new CommandValidationException($"Set Argument's property '{propertyName}' " 97 | + "is not writable. Try adding a setter.", 98 | CommandValidationError.PropertyNotWritable, commandLevel); 99 | } 100 | } 101 | 102 | internal static void ValuesMustBeSet(SetArgument argument, 103 | int commandLevel)where TRunInfo : class 104 | { 105 | if (argument.Values == null) 106 | { 107 | throw new CommandValidationException("List of values for the set must be provided.", 108 | CommandValidationError.NullObject, commandLevel); 109 | } 110 | } 111 | 112 | internal static void ValuesMustContainAtLeastTwoItems(SetArgument argument, 113 | int commandLevel)where TRunInfo : class 114 | { 115 | if (argument.Values.Count <= 1) 116 | { 117 | throw new CommandValidationException("Set Arguments must contain at least two items.", 118 | CommandValidationError.InsufficientCount, commandLevel); 119 | } 120 | } 121 | 122 | internal static void ValueLabelsMustBeUnique(SetArgument argument, 123 | int commandLevel)where TRunInfo : class 124 | { 125 | if (argument.Values.Select(v => v.Label).Distinct().Count() != argument.Values.Count) 126 | { 127 | throw new CommandValidationException("Set Argument value labels must be unique within a set.", 128 | CommandValidationError.DuplicateKey, commandLevel); 129 | } 130 | } 131 | 132 | internal static void ValueValuesMustBeUnique(SetArgument argument, 133 | int commandLevel)where TRunInfo : class 134 | { 135 | if (argument.Values.Select(v => v.Value).Distinct().Count() != argument.Values.Count) 136 | { 137 | throw new CommandValidationException("Set Argument values must be unique within a set.", 138 | CommandValidationError.DuplicateKey, commandLevel); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Source/Configuration/Validators/Rules/CommandRules.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace R5.RunInfoBuilder.Configuration.Validators.Rules 6 | { 7 | internal static class CommandRules 8 | { 9 | internal static class SubCommand 10 | { 11 | internal static void SubCommandsCannotBeNull(StackableCommand command, 12 | int commandLevel) where TRunInfo : class 13 | { 14 | int nullIndex = command.SubCommands.IndexOfFirstNull(); 15 | if (nullIndex != -1) 16 | { 17 | throw new CommandValidationException( 18 | $"Command contains a null subcommand (index {nullIndex}).", 19 | CommandValidationError.NullObject, commandLevel, nullIndex); 20 | } 21 | } 22 | 23 | internal static void SubCommandKeysMustBeUnique(StackableCommand command, 24 | int commandLevel) where TRunInfo : class 25 | { 26 | bool hasDuplicate = command.SubCommands.Count != command.SubCommands.Select(c => c.Key).Distinct().Count(); 27 | if (hasDuplicate) 28 | { 29 | throw new CommandValidationException("Command key is invalid because " 30 | + "it clashes with an already configured key.", 31 | CommandValidationError.DuplicateKey, commandLevel); 32 | } 33 | } 34 | } 35 | 36 | internal static class Common 37 | { 38 | internal static void KeyIsNotNullOrEmpty(StackableCommand command, 39 | int commandLevel) where TRunInfo : class 40 | { 41 | if (string.IsNullOrWhiteSpace(command.Key)) 42 | { 43 | throw new CommandValidationException("Command key must be provided.", 44 | CommandValidationError.KeyNotProvided, commandLevel); 45 | } 46 | } 47 | 48 | internal static void ArgumentsCannotBeNull(CommandBase command, 49 | int commandLevel) where TRunInfo : class 50 | { 51 | int nullIndex = command.Arguments.IndexOfFirstNull(); 52 | if (nullIndex != -1) 53 | { 54 | throw new CommandValidationException( 55 | $"Command contains a null argument (index {nullIndex}).", 56 | CommandValidationError.NullObject, commandLevel, nullIndex); 57 | } 58 | } 59 | 60 | internal static void OptionsCannotBeNull(List> options, 61 | int commandLevel) where TRunInfo : class 62 | { 63 | int nullIndex = options.IndexOfFirstNull(); 64 | if (nullIndex != -1) 65 | { 66 | throw new CommandValidationException( 67 | $"Command contains a null option (index {nullIndex}).", 68 | CommandValidationError.NullObject, commandLevel, nullIndex); 69 | } 70 | } 71 | 72 | internal static void OptionKeysMustMatchRegex(List> options, 73 | int commandLevel) where TRunInfo : class 74 | { 75 | bool matchesRegex = options 76 | .Select(o => o.Key) 77 | .All(OptionTokenizer.IsValidConfiguration); 78 | 79 | if (!matchesRegex) 80 | { 81 | throw new CommandValidationException("Command contains an option with an invalid key.", 82 | CommandValidationError.InvalidKey, commandLevel); 83 | } 84 | } 85 | 86 | internal static void OptionKeysMustBeUnique(List> options, 87 | List> otherOptions, int commandLevel) 88 | where TRunInfo : class 89 | { 90 | var fullKeys = new List(); 91 | var shortKeys = new List(); 92 | 93 | if (otherOptions != null) 94 | { 95 | options = options.Concat(otherOptions).ToList(); 96 | } 97 | 98 | options.ForEach(o => 99 | { 100 | var (fullKey, shortKey) = OptionTokenizer.TokenizeKeyConfiguration(o.Key); 101 | 102 | fullKeys.Add(fullKey); 103 | 104 | if (shortKey.HasValue) 105 | { 106 | shortKeys.Add(shortKey.Value); 107 | } 108 | }); 109 | 110 | bool duplicateFull = fullKeys.Count != fullKeys.Distinct().Count(); 111 | if (duplicateFull) 112 | { 113 | throw new CommandValidationException("Command contains options with duplicate full keys.", 114 | CommandValidationError.DuplicateKey, commandLevel); 115 | } 116 | 117 | bool duplicateShort = shortKeys.Count != shortKeys.Distinct().Count(); 118 | if (duplicateShort) 119 | { 120 | throw new CommandValidationException("Command contains options with duplicate short keys.", 121 | CommandValidationError.DuplicateKey, commandLevel); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Source/Configuration/Validators/Rules/OptionRules.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor; 2 | 3 | namespace R5.RunInfoBuilder.Configuration.Validators.Rules 4 | { 5 | internal static class OptionRules 6 | { 7 | internal static void PropertyMappingIsSet(Option option, 8 | int commandLevel) where TRunInfo : class 9 | { 10 | if (option.Property == null) 11 | { 12 | throw new CommandValidationException($"Option '{option.Key}' is missing its property mapping expression.", 13 | CommandValidationError.NullPropertyExpression, commandLevel); 14 | } 15 | } 16 | 17 | internal static void MappedPropertyIsWritable(Option option, 18 | int commandLevel) where TRunInfo : class 19 | { 20 | if (!ReflectionHelper.PropertyIsWritable(option.Property, out string propertyName)) 21 | { 22 | throw new CommandValidationException($"Option '{option.Key}'s property '{propertyName}' " 23 | + "is not writable. Try adding a setter.", 24 | CommandValidationError.PropertyNotWritable, commandLevel); 25 | } 26 | } 27 | 28 | internal static void OnProcessCallbackNotAllowedForBoolOptions(Option option, 29 | int commandLevel) where TRunInfo : class 30 | { 31 | if (option.OnParsed != null && typeof(TProperty) == typeof(bool)) 32 | { 33 | throw new CommandValidationException( 34 | "OnProcess callbacks aren't allowed on bool options.", 35 | CommandValidationError.CallbackNotAllowed, commandLevel); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Source/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder 6 | { 7 | public class ProcessException : Exception 8 | { 9 | public ProcessError ErrorType { get; } 10 | public int CommandLevel { get; } 11 | public new Exception InnerException { get; } 12 | public object[] Metadata { get; } 13 | 14 | public ProcessException( 15 | string message, 16 | ProcessError errorType = ProcessError.GeneralFailure, 17 | int commandLevel = -1, 18 | Exception innerException = null, 19 | params object[] metadata) 20 | : base(message) 21 | { 22 | ErrorType = errorType; 23 | CommandLevel = commandLevel; 24 | InnerException = innerException; 25 | Metadata = metadata; 26 | } 27 | } 28 | 29 | public enum ProcessError 30 | { 31 | GeneralFailure, 32 | ExpectedProgramArgument, 33 | ParserUnhandledType, 34 | ParserInvalidValue, 35 | InvalidStackedOption, 36 | ExpectedValueFoundOption, 37 | ExpectedValueFoundSubCommand, 38 | InvalidSubCommand, 39 | InvalidProgramArgument, 40 | InvalidStageResult, 41 | UnknownValue 42 | } 43 | 44 | 45 | // todo standard overloads 46 | public class CommandValidationException : Exception 47 | { 48 | public CommandValidationError ErrorType { get; } 49 | public int CommandLevel { get; } 50 | public object[] Metadata { get; } 51 | 52 | public CommandValidationException( 53 | string message, 54 | CommandValidationError errorType, 55 | int commandLevel, 56 | params object[] metadata) 57 | : base(message) 58 | { 59 | ErrorType = errorType; 60 | CommandLevel = commandLevel; 61 | Metadata = metadata; 62 | } 63 | } 64 | 65 | public enum CommandValidationError 66 | { 67 | RestrictedKey, 68 | KeyNotProvided, 69 | NullObject, 70 | DuplicateKey, 71 | InvalidKey, 72 | NullPropertyExpression, 73 | NullCustomHandler, 74 | InvalidCount, 75 | PropertyNotWritable, 76 | InvalidType, 77 | NullHelpToken, 78 | InsufficientCount, 79 | CallbackNotAllowed 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Source/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace R5.RunInfoBuilder 4 | { 5 | internal static class ListExtensionMethods 6 | { 7 | internal static int IndexOfFirstNull(this List list) 8 | { 9 | for (int i = 0; i < list.Count; i++) 10 | { 11 | if (list[i] == null) return i; 12 | } 13 | 14 | return -1; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Help/HelpBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.Help 7 | { 8 | internal static class HelpBuilder 9 | { 10 | private const string Padding = " "; 11 | private static string PaddingRepeated(int repeatCount) => new StringBuilder().Insert(0, Padding, repeatCount).ToString(); 12 | 13 | internal static string BuildFor(Command command, string programName) 14 | where TRunInfo : class 15 | { 16 | var sb = new StringBuilder(); 17 | 18 | sb.Append(command.Key); 19 | 20 | if (!string.IsNullOrWhiteSpace(command.Description)) 21 | { 22 | sb.AppendLine(" - " + command.Description); 23 | } 24 | else 25 | { 26 | sb.AppendLine(); 27 | } 28 | 29 | if (command.SubCommands.Any()) 30 | { 31 | sb.AppendLine(Padding + "Usage: "); 32 | } 33 | else 34 | { 35 | sb.Append(Padding + "Usage: "); 36 | } 37 | 38 | 39 | AppendCommandInfo(sb, command.Key, isRoot: true, command, commandDepth: 0, programName); 40 | 41 | return sb.ToString(); 42 | } 43 | 44 | private static void AppendCommandInfo(StringBuilder sb, 45 | string rootCommandKey, bool isRoot, StackableCommand command, int commandDepth, 46 | string programName) 47 | where TRunInfo : class 48 | { 49 | var helpTokens = new List(); 50 | 51 | if (!string.IsNullOrWhiteSpace(programName)) 52 | { 53 | helpTokens.Add(programName); 54 | } 55 | helpTokens.Add(rootCommandKey); 56 | 57 | helpTokens.AddRange(GetRepeatedList("...", commandDepth)); 58 | 59 | if (!isRoot) 60 | { 61 | helpTokens.Add(command.Key); 62 | } 63 | 64 | IEnumerable argumentTokens = command.Arguments.Select(a => a.GetHelpToken()); 65 | IEnumerable optionTokens = command.Options.Select(o => o.GetHelpToken()); 66 | 67 | helpTokens.AddRange(argumentTokens); 68 | helpTokens.AddRange(optionTokens); 69 | 70 | if (command.SubCommands.Any()) 71 | { 72 | var keys = string.Join("|", command.SubCommands.Select(c => c.Key)); 73 | helpTokens.Add($"({keys})"); 74 | helpTokens.Add("..."); 75 | } 76 | 77 | string result = string.Join(" ", helpTokens.Select(t => t.Trim())); 78 | 79 | if (command.SubCommands.Any()) 80 | { 81 | sb.AppendLine(PaddingRepeated(2) + result); 82 | } 83 | else if (!command.SubCommands.Any() && isRoot) 84 | { 85 | sb.AppendLine(result); 86 | } 87 | else 88 | { 89 | sb.AppendLine(PaddingRepeated(2) + result); 90 | } 91 | 92 | // recursively add subcommands with more padding 93 | foreach(SubCommand subCommand in command.SubCommands) 94 | { 95 | AppendCommandInfo(sb, rootCommandKey, isRoot: false, subCommand, commandDepth + 1, programName); 96 | } 97 | } 98 | 99 | private static List GetRepeatedList(string token, int count) 100 | { 101 | var result = new List(); 102 | while (count-- > 0) 103 | { 104 | result.Add(token); 105 | } 106 | return result; 107 | } 108 | 109 | internal static string BuildFor(DefaultCommand command, string programName) 110 | where TRunInfo : class 111 | { 112 | var sb = new StringBuilder(); 113 | 114 | sb.AppendLine("Default Command"); 115 | 116 | if (!string.IsNullOrWhiteSpace(command.Description)) 117 | { 118 | sb.AppendLine(Padding + $"{command.Description}"); 119 | } 120 | 121 | sb.Append(Padding + "Usage: "); 122 | 123 | var helpTokens = new List(); 124 | 125 | if (!string.IsNullOrWhiteSpace(programName)) 126 | { 127 | helpTokens.Add(programName); 128 | } 129 | 130 | helpTokens.AddRange(command.Arguments.Select(a => a.GetHelpToken())); 131 | helpTokens.AddRange(command.Options.Select(o => o.GetHelpToken())); 132 | 133 | var tokens = string.Join(" ", helpTokens); 134 | sb.Append(tokens); 135 | 136 | return sb.ToString(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Source/Help/HelpManager.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Help; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace R5.RunInfoBuilder 8 | { 9 | /// 10 | /// Provides methods to configure how the help menu works. 11 | /// 12 | public class HelpManager 13 | { 14 | private static readonly string[] _defaultTriggers = new string[] 15 | { 16 | "--help", "-h", "/help" 17 | }; 18 | 19 | internal bool InvokeOnFail { get; private set; } 20 | internal bool SuppressException { get; private set; } 21 | 22 | private string _programName { get; set; } 23 | private List _triggers { get; } 24 | private Action _customCallback { get; set; } 25 | private List _commandInfos { get; } 26 | private string _defaultCommandInfo { get; set; } 27 | 28 | internal HelpManager() 29 | { 30 | _triggers = new List(_defaultTriggers); 31 | _commandInfos = new List(); 32 | } 33 | 34 | /// 35 | /// Sets a value for your program's name for use in the help menu. 36 | /// 37 | /// The name value. 38 | /// The HelpManager instance. 39 | public HelpManager SetProgramName(string name) 40 | { 41 | if (string.IsNullOrWhiteSpace(name)) 42 | { 43 | throw new ArgumentNullException(nameof(name), "Program name must be provided."); 44 | } 45 | 46 | _programName = name; 47 | return this; 48 | } 49 | 50 | /// 51 | /// Sets the list of keywords that will trigger the help menu as a command. 52 | /// 53 | /// List of triggers as params. 54 | /// The HelpManager instance. 55 | public HelpManager SetTriggers(params string[] triggers) 56 | { 57 | if (triggers == null || !triggers.Any()) 58 | { 59 | throw new ArgumentNullException(nameof(triggers), "Triggers must be provided."); 60 | } 61 | 62 | _triggers.Clear(); 63 | _triggers.AddRange(triggers); 64 | 65 | return this; 66 | } 67 | 68 | /// 69 | /// Configures the builder to automatically display the help menu (or invoke callback) on fail. 70 | /// 71 | /// If true, will only display help text while suppressing the exception from bubbling to the client. 72 | /// The HelpManager instance. 73 | public HelpManager InvokeOnBuildFail(bool suppressException) 74 | { 75 | InvokeOnFail = true; 76 | SuppressException = suppressException; 77 | return this; 78 | } 79 | 80 | /// 81 | /// Sets a custom callback that will be invoked when the help is triggered. 82 | /// Having this set prevents the default help menu from displaying. 83 | /// 84 | /// The custom callback action. 85 | /// The HelpManager instance. 86 | public HelpManager OnTrigger(Action customCallback) 87 | { 88 | _customCallback = customCallback ?? throw new ArgumentNullException(nameof(customCallback), "A valid custom help callback must be provided."); 89 | return this; 90 | } 91 | 92 | internal void Invoke() 93 | { 94 | if (_customCallback != null) 95 | { 96 | _customCallback(); 97 | return; 98 | } 99 | 100 | Console.WriteLine(GetHelpText()); 101 | } 102 | 103 | private string GetHelpText() 104 | { 105 | var sb = new StringBuilder(); 106 | 107 | if (!string.IsNullOrWhiteSpace(_defaultCommandInfo)) 108 | { 109 | sb.AppendLine(_defaultCommandInfo); 110 | sb.AppendLine(); 111 | } 112 | 113 | _commandInfos.ForEach(i => sb.AppendLine(i)); 114 | 115 | return sb.ToString(); 116 | } 117 | 118 | internal bool IsTrigger(string token) 119 | { 120 | return _triggers.Contains(token); 121 | } 122 | 123 | internal void ConfigureForCommand(Command command) 124 | where TRunInfo : class 125 | { 126 | string helpText = HelpBuilder.BuildFor(command, _programName); 127 | _commandInfos.Add(helpText); 128 | } 129 | 130 | internal void ConfigureForDefaultCommand(DefaultCommand defaultCommand) 131 | where TRunInfo : class 132 | { 133 | string helpText = HelpBuilder.BuildFor(defaultCommand, _programName); 134 | _defaultCommandInfo = helpText; 135 | } 136 | 137 | /// 138 | /// Returns the same help text that's displayed when help is invoked. 139 | /// 140 | public override string ToString() 141 | { 142 | return GetHelpText(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Source/Help/HelpTokenResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Help 6 | { 7 | internal static class HelpTokenResolver 8 | { 9 | internal static string ForPropertyArgument() 10 | { 11 | var typeString = GetTypeString(); 12 | return $"<{typeString}>"; 13 | } 14 | 15 | internal static string ForSequenceArgument() 16 | { 17 | var typeString = GetTypeString(); 18 | return $"<...{typeString}>"; 19 | } 20 | 21 | // todo: maybe use a dictionary of mappings, allowing the 22 | // user to self-configure their own 23 | private static string GetTypeString() 24 | { 25 | Type type = typeof(T); 26 | 27 | if (type == typeof(string)) 28 | { 29 | return "string"; 30 | } 31 | if (type == typeof(int)) 32 | { 33 | return "int"; 34 | } 35 | if (type == typeof(bool)) 36 | { 37 | return "bool"; 38 | } 39 | if (type == typeof(double)) 40 | { 41 | return "double"; 42 | } 43 | if (type == typeof(decimal)) 44 | { 45 | return "decimal"; 46 | } 47 | if (type == typeof(char)) 48 | { 49 | return "char"; 50 | } 51 | if (type == typeof(byte)) 52 | { 53 | return "byte"; 54 | } 55 | if (type == typeof(DateTime)) 56 | { 57 | return "date"; 58 | } 59 | 60 | return type.Name; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Source/Hooks/BuildHooks.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Hooks; 2 | using System; 3 | 4 | namespace R5.RunInfoBuilder 5 | { 6 | /// 7 | /// Provides methods to configure custom callbacks as hooks into 8 | /// various stages of the build process. 9 | /// 10 | public class BuildHooks 11 | { 12 | internal Action OnStart { get; private set; } 13 | internal ReturnsWithBase NullOrEmptyReturns { get; private set; } 14 | 15 | /// 16 | /// Set the callback that's fired at the very beginning of building. 17 | /// The program arguments are provided as the single argument to the callback. 18 | /// 19 | /// The callback to be invoked. 20 | public BuildHooks OnStartBuild(Action callback) 21 | { 22 | OnStart = callback ?? 23 | throw new ArgumentNullException(nameof(callback), "Callback must be provided."); 24 | 25 | return this; 26 | } 27 | 28 | /// 29 | /// Set the callback that's fired if program arguments is null or empty. 30 | /// The builder will return the object returned from the callback. 31 | /// 32 | /// The callback to be invoked. 33 | public BuildHooks ArgsNullOrEmptyReturns(Func callback) 34 | { 35 | if (callback == null) 36 | { 37 | throw new ArgumentNullException(nameof(callback), "Callback must be provided."); 38 | } 39 | 40 | NullOrEmptyReturns = new ReturnsWith(callback); 41 | return this; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Source/Hooks/ReturnsWith.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace R5.RunInfoBuilder.Hooks 4 | { 5 | internal class ReturnsWith : ReturnsWithBase 6 | { 7 | private Func _callback { get; } 8 | 9 | internal ReturnsWith(Func callback) 10 | { 11 | _callback = callback ?? throw new Exception(); 12 | } 13 | 14 | internal override object Invoke() 15 | { 16 | return _callback(); 17 | } 18 | } 19 | 20 | internal abstract class ReturnsWithBase 21 | { 22 | internal abstract object Invoke(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Parser/ArgumentParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.Parser 7 | { 8 | /// 9 | /// Provides methods to configure how program arguments are parsed 10 | /// into various Types. Can handle system types out of the box. 11 | /// 12 | public class ArgumentParser 13 | { 14 | private bool _enumParseIgnoreCase { get; set; } 15 | 16 | // Value is of type Func. Storing as an object 17 | // to both allow users to generically add predicates AND retrieve 18 | // and parse generically 19 | private Dictionary _predicatesMap { get; } 20 | 21 | internal ArgumentParser() 22 | { 23 | _predicatesMap = new Dictionary(); 24 | 25 | this.AddSystemTypePredicates(); 26 | } 27 | 28 | /// 29 | /// Call to set the auto parsing of enum Types to ignore case sensitivity. 30 | /// 31 | /// The Parser instance. 32 | public ArgumentParser EnumParsingIgnoresCase() 33 | { 34 | _enumParseIgnoreCase = true; 35 | return this; 36 | } 37 | 38 | /// 39 | /// Configures a custom predicate function that parses a string into T. 40 | /// 41 | /// The Type being configured to be parseable. 42 | /// 43 | /// Custom func used to parse the program argument string. 44 | /// The func should return a value tuple with the first item being a bool representing 45 | /// a successful parse. The second item is the parsed T value. 46 | /// 47 | /// The Parser instance. 48 | public ArgumentParser SetPredicateForType(Func predicateFunc) 49 | { 50 | Type type = typeof(T); 51 | 52 | if (_predicatesMap.ContainsKey(type)) 53 | { 54 | _predicatesMap[type] = predicateFunc; 55 | } 56 | else 57 | { 58 | _predicatesMap.Add(typeof(T), predicateFunc); 59 | } 60 | 61 | return this; 62 | } 63 | 64 | /// 65 | /// Parses a string into an object of the specified Type. 66 | /// 67 | /// Type of the object that the value should be parsed into. 68 | /// The string token attempting to be parsed. 69 | /// The parsed object as an out parameter. 70 | /// A bool representing whether the value was successfully parsed. 71 | public bool TryParseAs(Type type, string value, out object parsed) 72 | { 73 | if (type == null) 74 | { 75 | throw new ArgumentNullException(nameof(type), "Type must be provided."); 76 | } 77 | if (value == null) 78 | { 79 | throw new ArgumentNullException(nameof(value), "Value must be provided."); 80 | } 81 | 82 | parsed = null; 83 | 84 | Type nullableType = Nullable.GetUnderlyingType(type); 85 | 86 | if (nullableType != null && nullableType.IsEnum) 87 | { 88 | if (value == "") 89 | { 90 | parsed = null; 91 | return true; 92 | } 93 | 94 | try 95 | { 96 | parsed = Enum.Parse(nullableType, value, ignoreCase: _enumParseIgnoreCase); 97 | return true; 98 | } 99 | catch (Exception) 100 | { 101 | // ignore exception. Enum.TryParse with ignoreCase not available in netstandard2.0 102 | return false; 103 | } 104 | } 105 | else if (type.IsEnum) 106 | { 107 | try 108 | { 109 | parsed = Enum.Parse(type, value, ignoreCase: _enumParseIgnoreCase); 110 | return true; 111 | } 112 | catch (Exception) 113 | { 114 | // ignore exception. Enum.TryParse with ignoreCase not available in netstandard2.0 115 | return false; 116 | } 117 | } 118 | 119 | if (!_predicatesMap.ContainsKey(type)) 120 | { 121 | throw new InvalidOperationException($"Predicate for type '{type.Name}' is not set."); 122 | } 123 | 124 | dynamic predicate = _predicatesMap[type]; 125 | 126 | var result = predicate.Invoke(value); 127 | 128 | bool valid = result.Item1; 129 | var parsedResult = result.Item2; 130 | 131 | if (valid) 132 | { 133 | parsed = parsedResult; 134 | return true; 135 | } 136 | 137 | return false; 138 | } 139 | 140 | /// 141 | /// Parses a string into an object of the specified Type. 142 | /// 143 | /// Type of the object that the value should be parsed into. 144 | /// The string token attempting to be parsed. 145 | /// The parsed object as an out parameter. 146 | /// A bool representing whether the value was successfully parsed. 147 | public bool TryParseAs(string value, out T parsed) 148 | { 149 | if (value == null) 150 | { 151 | throw new ArgumentNullException(nameof(value), "Value must be provided."); 152 | } 153 | 154 | parsed = default; 155 | 156 | Type type = typeof(T); 157 | Type nullableType = Nullable.GetUnderlyingType(type); 158 | 159 | if (nullableType != null && nullableType.IsEnum) 160 | { 161 | if (value == "") 162 | { 163 | parsed = default; 164 | return true; 165 | } 166 | 167 | try 168 | { 169 | parsed = (T)Enum.Parse(nullableType, value, ignoreCase: _enumParseIgnoreCase); 170 | return true; 171 | } 172 | catch (Exception) 173 | { 174 | // ignore exception. Enum.TryParse with ignoreCase not available in netstandard2.0 175 | return false; 176 | } 177 | } 178 | else if (type.IsEnum) 179 | { 180 | try 181 | { 182 | parsed = (T)Enum.Parse(type, value, ignoreCase: _enumParseIgnoreCase); 183 | return true; 184 | } 185 | catch (Exception) 186 | { 187 | // ignore exception info. Enum.TryParse with ignoreCase not available in netstandard2.0 188 | return false; 189 | } 190 | } 191 | 192 | if (!_predicatesMap.ContainsKey(type)) 193 | { 194 | throw new ArgumentException($"Predicate for type '{type.Name}' is not set.", nameof(type)); 195 | } 196 | 197 | var predicate = _predicatesMap[type] as Func; 198 | 199 | Debug.Assert(predicate != null, "Predicate should always be of the correct type."); 200 | 201 | (bool valid, T result) = predicate(value); 202 | 203 | if (valid) 204 | { 205 | parsed = result; 206 | return true; 207 | } 208 | 209 | return false; 210 | } 211 | 212 | /// 213 | /// Indicates whether the parser is currently configured to handle the Type. 214 | /// 215 | /// The type being determined to be parseable. 216 | /// A bool indicating whether the parser currently handles the Type. 217 | public bool HandlesType(Type type) 218 | { 219 | if (type == null) 220 | { 221 | throw new ArgumentNullException(nameof(type), "Type argument must be provided."); 222 | } 223 | 224 | return _predicatesMap.ContainsKey(type); 225 | } 226 | 227 | /// 228 | /// Indicates whether the parser is currently configured to handle the Type. 229 | /// 230 | /// The type being determined to be parseable. 231 | /// A bool indicating whether the parser currently handles the Type. 232 | public bool HandlesType() 233 | { 234 | return _predicatesMap.ContainsKey(typeof(T)); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Source/Parser/ArgumentParserExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Parser 6 | { 7 | internal static class ArgumentParserExtensions 8 | { 9 | internal static void AddSystemTypePredicates(this ArgumentParser parser) 10 | { 11 | // string 12 | parser.SetPredicateForType(value => (true, value)); 13 | 14 | // bool 15 | parser.SetPredicateForType(value => 16 | { 17 | var trueHash = new HashSet( 18 | new string[] { "yes", "y", "1" }, 19 | StringComparer.OrdinalIgnoreCase); 20 | 21 | var falseHash = new HashSet( 22 | new string[] { "no", "n", "0" }, 23 | StringComparer.OrdinalIgnoreCase); 24 | 25 | if (trueHash.Contains(value)) 26 | { 27 | return (true, true); 28 | } 29 | if (falseHash.Contains(value)) 30 | { 31 | return (true, false); 32 | } 33 | if (bool.TryParse(value, out bool parsed)) 34 | { 35 | return (true, parsed); 36 | } 37 | 38 | return (false, default); 39 | }); 40 | 41 | // byte 42 | parser.SetPredicateForType(value => 43 | { 44 | if (byte.TryParse(value, out byte parsed)) 45 | { 46 | return (true, parsed); 47 | } 48 | return (false, default); 49 | }); 50 | 51 | // char 52 | parser.SetPredicateForType(value => 53 | { 54 | if (char.TryParse(value, out char parsed)) 55 | { 56 | return (true, parsed); 57 | } 58 | return (false, default); 59 | }); 60 | 61 | // datetime 62 | parser.SetPredicateForType(val => 63 | { 64 | if (DateTime.TryParse(val, out DateTime parsed)) 65 | { 66 | return (true, parsed); 67 | } 68 | return (false, default); 69 | }); 70 | 71 | // decimal 72 | parser.SetPredicateForType(val => 73 | { 74 | if (decimal.TryParse(val, out decimal parsed)) 75 | { 76 | return (true, parsed); 77 | } 78 | return (false, default); 79 | }); 80 | 81 | // double 82 | parser.SetPredicateForType(val => 83 | { 84 | if (double.TryParse(val, out double parsed)) 85 | { 86 | return (true, parsed); 87 | } 88 | return (false, default); 89 | }); 90 | 91 | // int 92 | parser.SetPredicateForType(val => 93 | { 94 | if (int.TryParse(val, out int parsed)) 95 | { 96 | return (true, parsed); 97 | } 98 | return (false, default); 99 | }); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Source/Processor/Functions/OptionFunctions.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace R5.RunInfoBuilder.Processor.Functions 7 | { 8 | internal class OptionFunctions 9 | where TRunInfo : class 10 | { 11 | private Dictionary> _fullOptionInfo { get; } 12 | private Dictionary> _shortOptionInfo { get; } 13 | private HashSet _fullBoolTypeKeys { get; } 14 | private HashSet _shortBoolTypeKeys { get; } 15 | 16 | internal OptionFunctions(List> options) 17 | { 18 | _fullOptionInfo = new Dictionary>(); 19 | _shortOptionInfo = new Dictionary>(); 20 | _fullBoolTypeKeys = new HashSet(); 21 | _shortBoolTypeKeys = new HashSet(); 22 | 23 | InitializeMaps(options); 24 | } 25 | 26 | private void InitializeMaps(List> options) 27 | { 28 | foreach (OptionBase option in options) 29 | { 30 | addProcessInfo(option); 31 | 32 | if (option.Type == typeof(bool)) 33 | { 34 | addToBoolMaps(option); 35 | } 36 | } 37 | 38 | // local functions 39 | void addProcessInfo(OptionBase option) 40 | { 41 | var (fullKey, shortKey) = OptionTokenizer.TokenizeKeyConfiguration(option.Key); 42 | OptionProcessInfo processInfo = option.GetProcessInfo(); 43 | 44 | _fullOptionInfo.Add(fullKey, processInfo); 45 | 46 | if (shortKey != null) 47 | { 48 | _shortOptionInfo.Add(shortKey.Value, processInfo); 49 | } 50 | } 51 | 52 | void addToBoolMaps(OptionBase option) 53 | { 54 | var (fullKey, shortKey) = OptionTokenizer.TokenizeKeyConfiguration(option.Key); 55 | 56 | _fullBoolTypeKeys.Add(fullKey); 57 | 58 | if (shortKey != null) 59 | { 60 | _shortBoolTypeKeys.Add(shortKey.Value); 61 | } 62 | } 63 | } 64 | 65 | internal OptionProcessInfo GetOptionProcessInfo(string fullKey) 66 | { 67 | if (!_fullOptionInfo.TryGetValue(fullKey, out OptionProcessInfo processInfo)) 68 | { 69 | throw new InvalidOperationException($"'{fullKey}' is not a valid option full key."); 70 | } 71 | return processInfo; 72 | } 73 | 74 | internal OptionProcessInfo GetOptionProcessInfo(char shortKey) 75 | { 76 | if (!_shortOptionInfo.TryGetValue(shortKey, out OptionProcessInfo processInfo)) 77 | { 78 | throw new InvalidOperationException($"'{shortKey}' is not a valid option short key."); 79 | } 80 | return processInfo; 81 | } 82 | 83 | internal List> GetOptionProcessInfos(List stackedKeys) 84 | { 85 | var infos = new List>(); 86 | 87 | foreach (char key in stackedKeys) 88 | { 89 | if (!_shortOptionInfo.TryGetValue(key, out OptionProcessInfo processInfo)) 90 | { 91 | throw new InvalidOperationException($"'{key}' is not a valid option short key."); 92 | } 93 | 94 | if (processInfo.Type != typeof(bool)) 95 | { 96 | throw new InvalidOperationException($"Key '{key}' is part of a stacked option token but not mapped to a bool type."); 97 | } 98 | 99 | infos.Add(processInfo); 100 | } 101 | 102 | return infos; 103 | } 104 | 105 | internal bool IsOption(string programArgument) 106 | { 107 | try 108 | { 109 | (OptionType type, string fullKey, List shortKeys, _) = OptionTokenizer.TokenizeProgramArgument(programArgument); 110 | 111 | switch (type) 112 | { 113 | case OptionType.Full: 114 | return IsFullOption(fullKey); 115 | case OptionType.Short: 116 | return IsShortOption(shortKeys.Single()); 117 | case OptionType.Stacked: 118 | return shortKeys.Count == shortKeys.Distinct().Count() 119 | && shortKeys.All(_shortOptionInfo.ContainsKey); 120 | default: 121 | throw new ArgumentOutOfRangeException($"'{type}' is not a valid option type."); 122 | } 123 | } 124 | catch (ArgumentException) 125 | { 126 | return false; 127 | } 128 | 129 | // local functions 130 | bool IsFullOption(string s) => _fullOptionInfo.ContainsKey(s); 131 | 132 | bool IsShortOption(char c) => _shortOptionInfo.ContainsKey(c); 133 | } 134 | 135 | internal bool IsBoolType(string fullKey) => _fullBoolTypeKeys.Contains(fullKey); 136 | 137 | internal bool IsBoolType(char shortKey) => _shortBoolTypeKeys.Contains(shortKey); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Source/Processor/Functions/ProgramArgumentFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace R5.RunInfoBuilder.Processor.Functions 5 | { 6 | internal class ProgramArgumentFunctions 7 | { 8 | internal Func HasMore { get; } 9 | internal Func Peek { get; } 10 | internal Func Dequeue { get; } 11 | internal Func NextIsSubCommand { get; } 12 | internal Func NextIsOption { get; } 13 | 14 | internal ProgramArgumentFunctions( 15 | Func hasMoreFunc, 16 | Func peekFunc, 17 | Func dequeueFunc, 18 | HashSet subCommands, 19 | Func isOptionFunc) 20 | { 21 | HasMore = hasMoreFunc; 22 | Peek = peekFunc; 23 | Dequeue = dequeueFunc; 24 | NextIsSubCommand = () => subCommands.Contains(Peek()); 25 | NextIsOption = () => isOptionFunc(Peek()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Processor/Functions/StageFunctions.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Stages; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace R5.RunInfoBuilder.Processor.Functions 6 | { 7 | internal class StageFunctions 8 | where TRunInfo : class 9 | { 10 | internal Func HasMore { get; } 11 | internal Func> Dequeue { get; } 12 | internal Action>> ExtendPipelineWith { get; } 13 | 14 | internal StageFunctions( 15 | Func hasMoreFunc, 16 | Func> dequeueFunc, 17 | Action>> extendFunc) 18 | { 19 | HasMore = hasMoreFunc; 20 | Dequeue = dequeueFunc; 21 | ExtendPipelineWith = extendFunc; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Processor/Models/CustomHandlerContext.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Parser; 2 | using System.Collections.Generic; 3 | 4 | namespace R5.RunInfoBuilder 5 | { 6 | public class CustomHandlerContext where TRunInfo : class 7 | { 8 | public TRunInfo RunInfo { get; } 9 | public List ProgramArguments { get; } 10 | public ArgumentParser Parser { get; } 11 | 12 | internal CustomHandlerContext( 13 | TRunInfo runInfo, 14 | List handledProgramArguments, 15 | ArgumentParser parser) 16 | { 17 | RunInfo = runInfo; 18 | ProgramArguments = handledProgramArguments; 19 | Parser = parser; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/Processor/Models/OptionProcessInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.Processor.Models 6 | { 7 | internal class OptionProcessInfo 8 | where TRunInfo : class 9 | { 10 | internal Action Setter { get; } 11 | internal Type Type { get; } 12 | internal object OnParsed { get; } // type = Func 13 | internal Func OnParseErrorUseMessage { get; } 14 | 15 | internal OptionProcessInfo( 16 | Action setter, 17 | Type type, 18 | object onParsed, 19 | Func onParseErrorUseMessage) 20 | { 21 | Setter = setter; 22 | Type = type; 23 | OnParsed = onParsed; 24 | OnParseErrorUseMessage = onParseErrorUseMessage; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Source/Processor/Models/OptionType.cs: -------------------------------------------------------------------------------- 1 | namespace R5.RunInfoBuilder.Processor.Models 2 | { 3 | internal enum OptionType 4 | { 5 | Full, 6 | Short, 7 | Stacked 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/Processor/Models/ProcessContext.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Parser; 2 | using R5.RunInfoBuilder.Processor.Functions; 3 | using R5.RunInfoBuilder.Processor.Stages; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace R5.RunInfoBuilder.Processor.Models 9 | { 10 | internal class ProcessContext where TRunInfo : class 11 | { 12 | internal TRunInfo RunInfo { get; } 13 | internal int CommandLevel { get; } 14 | internal ArgumentParser Parser { get; } 15 | internal StageFunctions Stages { get; private set; } 16 | internal ProgramArgumentFunctions ProgramArguments { get; private set; } 17 | internal OptionFunctions Options { get; private set; } 18 | 19 | private Queue> _stages { get; } 20 | private Queue _programArguments { get; } 21 | private List> _globalOptions { get; } 22 | 23 | internal ProcessContext( 24 | TRunInfo runInfo, 25 | int commandLevel, 26 | ArgumentParser parser, 27 | Queue> stages, 28 | Queue programArguments, 29 | CommandBase command, 30 | List> globalOptions) 31 | { 32 | RunInfo = runInfo; 33 | CommandLevel = commandLevel; 34 | Parser = parser; 35 | _stages = stages; 36 | _programArguments = programArguments; 37 | _globalOptions = globalOptions; 38 | 39 | List> options = command.Options; 40 | if (globalOptions != null) 41 | { 42 | options = options.Concat(globalOptions).ToList(); 43 | } 44 | 45 | InitializeStageFunctions(); 46 | InitializeOptionFunctions(options); 47 | InitializeProgramArgumentFunctions(command); 48 | } 49 | 50 | internal ProcessContext RecreateForCommand(CommandBase command) 51 | { 52 | return new ProcessContext( 53 | RunInfo, CommandLevel + 1, Parser, _stages, _programArguments, command, _globalOptions); 54 | } 55 | 56 | private void InitializeStageFunctions() 57 | { 58 | Action>> extendPipelineFunc = newStages => 59 | { 60 | while (newStages.Any()) 61 | { 62 | _stages.Enqueue(newStages.Dequeue()); 63 | } 64 | }; 65 | 66 | Stages = new StageFunctions( 67 | _stages.Any, 68 | _stages.Dequeue, 69 | extendPipelineFunc); 70 | } 71 | 72 | private void InitializeOptionFunctions(List> options) 73 | { 74 | Options = new OptionFunctions(options); 75 | } 76 | 77 | private void InitializeProgramArgumentFunctions(CommandBase command) 78 | { 79 | var subCommands = command is StackableCommand cmd 80 | ? new HashSet(cmd.SubCommands.Select(c => c.Key)) 81 | : new HashSet(); 82 | 83 | ProgramArguments = new ProgramArgumentFunctions( 84 | _programArguments.Any, 85 | _programArguments.Peek, 86 | _programArguments.Dequeue, 87 | subCommands, 88 | Options.IsOption); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/Processor/Models/ProcessStageResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder 6 | { 7 | public static class ProcessResult 8 | { 9 | public static readonly ProcessStageResult Continue = new Continue(); 10 | public static readonly ProcessStageResult End = new End(); 11 | } 12 | 13 | public abstract class ProcessStageResult { } 14 | 15 | internal class Continue : ProcessStageResult { } 16 | 17 | internal class End : ProcessStageResult { } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Processor/OptionSetterFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace R5.RunInfoBuilder.Processor 5 | { 6 | internal static class OptionSetterFactory 7 | where TRunInfo : class 8 | { 9 | internal static (Action Setter, Type Type) CreateSetter(OptionBase option) 10 | { 11 | dynamic opt = option; 12 | PropertyInfo propertyInfo = ReflectionHelper.GetPropertyInfoFromExpression(opt.Property); 13 | 14 | Type valueType = propertyInfo.PropertyType; 15 | 16 | Action setter = (runInfo, value) => 17 | { 18 | bool isNullableEnum = Nullable.GetUnderlyingType(valueType)?.IsEnum == true; 19 | 20 | if (isNullableEnum) 21 | { 22 | if (value != null && value.GetType() != Nullable.GetUnderlyingType(valueType)) 23 | { 24 | throw new InvalidOperationException($"'{value}' is not a valid '{valueType}' type."); 25 | } 26 | } 27 | else 28 | { 29 | if (value.GetType() != valueType) 30 | { 31 | throw new InvalidOperationException($"'{value}' is not a valid '{valueType}' type."); 32 | } 33 | } 34 | 35 | //if (!(isNullableEnum && value == null) && value.GetType() != valueType) 36 | //{ 37 | // throw new InvalidOperationException($"'{value}' is not a valid '{valueType}' type."); 38 | //} 39 | 40 | propertyInfo.SetValue(runInfo, value); 41 | }; 42 | 43 | return (setter, valueType); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Source/Processor/OptionTokenizer.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace R5.RunInfoBuilder.Processor 9 | { 10 | internal static class OptionTokenizer 11 | { 12 | internal const string OptionConfigurationRegex = @"^(\s+)?[A-Za-z0-9_]([A-Za-z0-9-_]+)?(\s+)?([|](\s+)?[A-Za-z0-9](\s+)?)?$"; 13 | 14 | internal static bool IsValidConfiguration(string input) 15 | { 16 | return !string.IsNullOrWhiteSpace(input) && Regex.IsMatch(input, OptionTokenizer.OptionConfigurationRegex); 17 | } 18 | 19 | // assumes input is valid, shoudl be validated before here! 20 | internal static (string FullKey, char? ShortKey) TokenizeKeyConfiguration(string input) 21 | { 22 | if (input.Contains("|")) 23 | { 24 | string[] split = input.Split('|'); 25 | return (split[0].Trim(), split[1].Trim().ToCharArray().Single()); 26 | } 27 | 28 | return (input.Trim(), null); 29 | } 30 | 31 | internal static (OptionType Type, string FullKey, List ShortKeys, string Value) TokenizeProgramArgument(string argument) 32 | { 33 | if (!argument.StartsWith("--") && !argument.StartsWith("-")) 34 | { 35 | throw new ArgumentException("Options must begin with '--' or '-'.", nameof(argument)); 36 | } 37 | 38 | char[] chars = argument.ToCharArray(); 39 | if (chars.Count(c => c == '=') > 1 || chars.Last() == '=') 40 | { 41 | throw new ArgumentException("Options can only contain a single '=' and must not be the last character.", nameof(argument)); 42 | } 43 | 44 | int invalidEqualsIndex = argument.StartsWith("--") ? 2 : 1; 45 | if (argument[invalidEqualsIndex] == '=') 46 | { 47 | throw new ArgumentException("Option keys cannot begin with '='."); 48 | } 49 | 50 | string value = null; 51 | 52 | var keyValueSplit = argument.Split('='); 53 | if (keyValueSplit.Length == 2) 54 | { 55 | value = keyValueSplit[1]; 56 | } 57 | 58 | string key = keyValueSplit[0]; 59 | 60 | if (key.StartsWith("--")) 61 | { 62 | return (OptionType.Full, key.Substring(2), null, value); 63 | } 64 | 65 | if (key.Length == 2) 66 | { 67 | return (OptionType.Short, null, new List { key[1] }, value); 68 | } 69 | 70 | List stackedKeys = key.Skip(1).ToList(); 71 | if (stackedKeys.Count != stackedKeys.Distinct().Count()) 72 | { 73 | throw new ArgumentException($"Stacked key token '{key}' contains duplicates."); 74 | } 75 | 76 | return (OptionType.Stacked, null, stackedKeys, value); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Processor/Pipeline.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Parser; 2 | using R5.RunInfoBuilder.Processor.Models; 3 | using R5.RunInfoBuilder.Processor.Stages; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace R5.RunInfoBuilder.Processor 10 | { 11 | internal class Pipeline 12 | where TRunInfo : class 13 | { 14 | private Queue> _stages { get; } 15 | private string[] _args { get; } 16 | private Queue _programArguments { get; } 17 | private CommandBase _initialCommand { get; } 18 | private ArgumentParser _parser { get; } 19 | private List> _globalOptions { get; } 20 | 21 | internal Pipeline( 22 | Queue> stages, 23 | string[] args, 24 | CommandBase initialCommand, 25 | ArgumentParser parser, 26 | List> globalOptions) 27 | { 28 | _stages = stages; 29 | _args = args; 30 | _programArguments = new Queue(args); 31 | _initialCommand = initialCommand; 32 | _parser = parser; 33 | _globalOptions = globalOptions; 34 | } 35 | 36 | internal TRunInfo Process() 37 | { 38 | TRunInfo runInfo = (TRunInfo)Activator.CreateInstance(typeof(TRunInfo)); 39 | 40 | ProcessContext context = new ProcessContext( 41 | runInfo, 0, _parser, _stages, _programArguments, _initialCommand, _globalOptions); 42 | 43 | Action> resetContextFunc = cmd => 44 | { 45 | context = context.RecreateForCommand(cmd); 46 | }; 47 | 48 | bool ended = false; 49 | while (_stages.Any()) 50 | { 51 | Stage current = _stages.Dequeue(); 52 | 53 | ProcessStageResult result = current.ProcessStage(context, resetContextFunc); 54 | 55 | switch (result) 56 | { 57 | case Continue _: 58 | break; 59 | case End _: 60 | ended = true; 61 | break; 62 | case null: 63 | default: 64 | throw new ProcessException( 65 | "Current stage processing returned an invalid result.", 66 | ProcessError.InvalidStageResult, context.CommandLevel); 67 | } 68 | 69 | if (ended) 70 | { 71 | break; 72 | } 73 | } 74 | 75 | return runInfo; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Source/Processor/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | namespace R5.RunInfoBuilder.Processor 8 | { 9 | internal static class ReflectionHelper 10 | where TRunInfo : class 11 | { 12 | internal static bool PropertyIsWritable(Expression> propertyExpression, out string propertyName) 13 | { 14 | PropertyInfo propertyInfo = GetPropertyInfoFromExpression(propertyExpression); 15 | propertyName = propertyInfo.Name; 16 | 17 | return propertyInfo.CanWrite; 18 | } 19 | 20 | internal static PropertyInfo GetPropertyInfoFromExpression(Expression> propertyExpression) 21 | { 22 | var memberExpression = propertyExpression.Body as MemberExpression; 23 | if (memberExpression == null) 24 | { 25 | throw new ArgumentException($"Failed to read the property expression body as a '{nameof(MemberExpression)}' type."); 26 | } 27 | 28 | var propertyInfo = memberExpression.Member as PropertyInfo; 29 | if (propertyInfo == null) 30 | { 31 | throw new ArgumentException($"Failed to read the member expression as a '{nameof(PropertyInfo)}' type."); 32 | } 33 | 34 | return propertyInfo; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/Processor/Stages/CommandStage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | 4 | namespace R5.RunInfoBuilder.Processor.Stages 5 | { 6 | internal class CommandStage : Stage 7 | where TRunInfo : class 8 | { 9 | public Func _onMatched { get; } 10 | 11 | internal CommandStage(Func onMatched) 12 | { 13 | _onMatched = onMatched; 14 | } 15 | 16 | internal override ProcessStageResult ProcessStage(ProcessContext context, 17 | Action> resetContextFunc) 18 | { 19 | ProcessStageResult onMatchedResult = _onMatched?.Invoke(context.RunInfo); 20 | if (onMatchedResult == ProcessResult.End) 21 | { 22 | return ProcessResult.End; 23 | } 24 | 25 | return ProcessResult.Continue; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Processor/Stages/CustomArgumentStage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace R5.RunInfoBuilder.Processor.Stages 6 | { 7 | internal class CustomArgumentStage : Stage 8 | where TRunInfo : class 9 | { 10 | private int _count { get; } 11 | private Func, ProcessStageResult> _handler { get; } 12 | 13 | internal CustomArgumentStage( 14 | int count, 15 | Func, ProcessStageResult> handler) 16 | { 17 | _count = count; 18 | _handler = handler; 19 | } 20 | 21 | internal override ProcessStageResult ProcessStage(ProcessContext context, 22 | Action> resetContextFunc) 23 | { 24 | CustomHandlerContext handlerContext = GetCustomHandlerContext(context); 25 | 26 | return _handler(handlerContext); 27 | } 28 | 29 | private CustomHandlerContext GetCustomHandlerContext(ProcessContext context) 30 | { 31 | var handledArguments = new List(); 32 | 33 | int count = _count; 34 | while (count > 0) 35 | { 36 | if (!context.ProgramArguments.HasMore()) 37 | { 38 | throw new ProcessException( 39 | "Reached the end of program arguments while processing the custom argument stage.", 40 | ProcessError.ExpectedProgramArgument, context.CommandLevel); 41 | } 42 | 43 | handledArguments.Add(context.ProgramArguments.Dequeue()); 44 | count--; 45 | } 46 | 47 | return new CustomHandlerContext(context.RunInfo, handledArguments, context.Parser); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/Processor/Stages/DefaultCommandStage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using R5.RunInfoBuilder.Processor.Models; 3 | 4 | namespace R5.RunInfoBuilder.Processor.Stages 5 | { 6 | internal class DefaultCommandStage : Stage 7 | where TRunInfo : class 8 | { 9 | public Func _onMatched { get; } 10 | 11 | internal DefaultCommandStage(Func onMatched) 12 | { 13 | _onMatched = onMatched; 14 | } 15 | 16 | internal override ProcessStageResult ProcessStage(ProcessContext context, 17 | Action> resetContextFunc) 18 | { 19 | ProcessStageResult onMatchedResult = _onMatched?.Invoke(context.RunInfo); 20 | if (onMatchedResult == ProcessResult.End) 21 | { 22 | return ProcessResult.End; 23 | } 24 | 25 | return ProcessResult.Continue; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Processor/Stages/EndProcessStage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.Processor.Stages 7 | { 8 | internal class EndProcessStage : Stage 9 | where TRunInfo : class 10 | { 11 | private Action _postBuildCallback { get; } 12 | 13 | internal EndProcessStage(Action postBuildCallback) 14 | { 15 | _postBuildCallback = postBuildCallback; 16 | } 17 | 18 | internal override ProcessStageResult ProcessStage(ProcessContext context, 19 | Action> resetContextFunc) 20 | { 21 | if (context.ProgramArguments.HasMore()) 22 | { 23 | var invalidArgument = context.ProgramArguments.Peek(); 24 | throw new ProcessException($"Invalid program argument found: {invalidArgument}", 25 | ProcessError.InvalidProgramArgument, context.CommandLevel); 26 | } 27 | 28 | _postBuildCallback?.Invoke(context.RunInfo); 29 | 30 | return ProcessResult.End; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Source/Processor/Stages/OptionStage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using R5.RunInfoBuilder.Parser; 6 | using R5.RunInfoBuilder.Processor.Models; 7 | 8 | namespace R5.RunInfoBuilder.Processor.Stages 9 | { 10 | internal class OptionStage : Stage 11 | where TRunInfo : class 12 | { 13 | internal OptionStage() 14 | { 15 | } 16 | 17 | internal override ProcessStageResult ProcessStage(ProcessContext context, 18 | Action> resetContextFunc) 19 | { 20 | while (context.ProgramArguments.HasMore()) 21 | { 22 | if (context.ProgramArguments.NextIsSubCommand()) 23 | { 24 | return ProcessResult.Continue; 25 | } 26 | 27 | if (!context.ProgramArguments.NextIsOption()) 28 | { 29 | return ProcessResult.Continue; // results are optional so just continue 30 | } 31 | 32 | string option = context.ProgramArguments.Dequeue(); 33 | 34 | var (type, fullKey, shortKeys, valueFromToken) = OptionTokenizer.TokenizeProgramArgument(option); 35 | 36 | bool isBoolType = fullKey != null 37 | ? context.Options.IsBoolType(fullKey) 38 | : shortKeys.All(context.Options.IsBoolType); 39 | 40 | if (type == OptionType.Stacked && !isBoolType) 41 | { 42 | throw new ProcessException($"Stacked options can only be mapped to " 43 | + $"boolean properties but found one or more invalid options in: {string.Join("", shortKeys)}", 44 | ProcessError.InvalidStackedOption, context.CommandLevel); 45 | } 46 | 47 | string value = ResolveValue(valueFromToken, isBoolType, context); 48 | 49 | ProcessStageResult optionResult; 50 | switch (type) 51 | { 52 | case OptionType.Full: 53 | optionResult = ProcessFull(fullKey, value, context); 54 | break; 55 | case OptionType.Short: 56 | optionResult = ProcessShort(shortKeys.Single(), value, context); 57 | break; 58 | case OptionType.Stacked: 59 | optionResult = ProcessStacked(shortKeys, value, context); 60 | break; 61 | default: 62 | throw new ArgumentOutOfRangeException(nameof(type), $"'{type}' is not a valid option type."); 63 | } 64 | 65 | switch (optionResult) 66 | { 67 | case Continue _: 68 | break; 69 | case End _: 70 | return ProcessResult.End; 71 | case null: 72 | default: 73 | throw new ProcessException( 74 | "Option OnProcess callback returned an unknown result.", 75 | ProcessError.InvalidStageResult, context.CommandLevel); 76 | } 77 | } 78 | 79 | return ProcessResult.Continue; 80 | } 81 | 82 | private string ResolveValue(string valueFromToken, bool isBoolTypeOption, 83 | ProcessContext context) 84 | { 85 | if (!string.IsNullOrWhiteSpace(valueFromToken)) 86 | { 87 | return valueFromToken; 88 | } 89 | 90 | if (!context.ProgramArguments.HasMore()) 91 | { 92 | if (!isBoolTypeOption) 93 | { 94 | throw new ProcessException("Expected a value but reached the end of program args.", 95 | ProcessError.ExpectedProgramArgument, context.CommandLevel); 96 | } 97 | 98 | // TODO: need the parser to always handle some strnig constant for true 99 | // if the user resets the predicate, we need to also add our own for 100 | // handling this case 101 | return "true"; 102 | } 103 | 104 | // more program args exist 105 | string next = context.ProgramArguments.Peek(); 106 | 107 | if (context.ProgramArguments.NextIsOption()) 108 | { 109 | if (!isBoolTypeOption) 110 | { 111 | throw new ProcessException("Expected a value for the next program argument " 112 | + $"but found an option instead: '{next}'", 113 | ProcessError.ExpectedValueFoundOption, context.CommandLevel); 114 | } 115 | 116 | return "true"; 117 | } 118 | 119 | if (context.ProgramArguments.NextIsSubCommand()) 120 | { 121 | if (!isBoolTypeOption) 122 | { 123 | throw new ProcessException("Expected a value for the next program argument " 124 | + $"but found an sub command instead: '{next}'", 125 | ProcessError.ExpectedValueFoundSubCommand, context.CommandLevel); 126 | } 127 | 128 | return "true"; 129 | } 130 | 131 | return context.ProgramArguments.Dequeue(); 132 | } 133 | 134 | private ProcessStageResult ProcessFull(string key, string valueString, ProcessContext context) 135 | { 136 | OptionProcessInfo processInfo = context.Options.GetOptionProcessInfo(key); 137 | 138 | object value = GetParsedValue(processInfo.Type, valueString, context, processInfo.OnParseErrorUseMessage); 139 | 140 | if (processInfo.OnParsed != null) 141 | { 142 | dynamic convertedType = Convert.ChangeType(value, processInfo.Type); 143 | 144 | dynamic onProcess = processInfo.OnParsed; 145 | 146 | ProcessStageResult onProcessResult = onProcess.Invoke(convertedType); 147 | if (onProcessResult == ProcessResult.End) 148 | { 149 | return ProcessResult.End; 150 | } 151 | } 152 | 153 | processInfo.Setter(context.RunInfo, value); 154 | 155 | return ProcessResult.Continue; 156 | } 157 | 158 | private ProcessStageResult ProcessShort(char key, string valueString, ProcessContext context) 159 | { 160 | OptionProcessInfo processInfo = context.Options.GetOptionProcessInfo(key); 161 | 162 | object value = GetParsedValue(processInfo.Type, valueString, context, processInfo.OnParseErrorUseMessage); 163 | 164 | if (processInfo.OnParsed != null) 165 | { 166 | dynamic convertedType = Convert.ChangeType(value, processInfo.Type); 167 | 168 | dynamic onProcess = processInfo.OnParsed; 169 | 170 | ProcessStageResult onProcessResult = onProcess.Invoke(convertedType); 171 | if (onProcessResult == ProcessResult.End) 172 | { 173 | return ProcessResult.End; 174 | } 175 | } 176 | 177 | processInfo.Setter(context.RunInfo, value); 178 | 179 | return ProcessResult.Continue; 180 | } 181 | 182 | private ProcessStageResult ProcessStacked(List keys, string valueString, ProcessContext context) 183 | { 184 | List> processInfos = context.Options.GetOptionProcessInfos(keys); 185 | 186 | object value = GetParsedValue(processInfos.First().Type, valueString, 187 | context, processInfos.First().OnParseErrorUseMessage); 188 | 189 | foreach(Action setter in processInfos.Select(i => i.Setter)) 190 | { 191 | setter(context.RunInfo, value); 192 | } 193 | 194 | return ProcessResult.Continue; 195 | } 196 | 197 | private object GetParsedValue(Type valueType, string valueString, 198 | ProcessContext context, Func onParseErrorUseMessage) 199 | { 200 | int commandLevel = context.CommandLevel; 201 | ArgumentParser parser = context.Parser; 202 | 203 | if (valueType == typeof(string)) 204 | { 205 | return valueString; 206 | } 207 | 208 | if (valueType == typeof(bool)) 209 | { 210 | return getForBoolType(); 211 | } 212 | 213 | return getByParsing(); 214 | 215 | // local functions 216 | bool getForBoolType() 217 | { 218 | if (valueString == null) 219 | { 220 | return true; 221 | } 222 | 223 | if (!parser.TryParseAs(valueString, out bool parsed)) 224 | { 225 | string message = onParseErrorUseMessage != null 226 | ? onParseErrorUseMessage(valueString) 227 | : $"'{valueString}' could not be parsed as a 'bool' type."; 228 | 229 | throw new ProcessException(message, ProcessError.ParserInvalidValue, commandLevel); 230 | } 231 | return parsed; 232 | } 233 | 234 | object getByParsing() 235 | { 236 | if (!parser.TryParseAs(valueType, valueString, out object parsed)) 237 | { 238 | string message = onParseErrorUseMessage != null 239 | ? onParseErrorUseMessage(valueString) 240 | : $"'{valueString}' could not be parsed as a '{valueType.Name}' type."; 241 | 242 | throw new ProcessException(message, ProcessError.ParserInvalidValue, commandLevel); 243 | } 244 | 245 | return parsed; 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Source/Processor/Stages/PropertyArgumentStage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace R5.RunInfoBuilder.Processor.Stages 9 | { 10 | internal class PropertyArgumentStage : Stage 11 | where TRunInfo : class 12 | { 13 | private Expression> _property { get; } 14 | private Func _onParsed { get; } 15 | private Func _onParseErrorUseMessage { get; } 16 | 17 | internal PropertyArgumentStage( 18 | Expression> property, 19 | Func onParsed, 20 | Func onParseErrorUseMessage) 21 | { 22 | _property = property; 23 | _onParsed = onParsed; 24 | _onParseErrorUseMessage = onParseErrorUseMessage; 25 | } 26 | 27 | internal override ProcessStageResult ProcessStage(ProcessContext context, 28 | Action> resetContextFunc) 29 | { 30 | if (!context.ProgramArguments.HasMore()) 31 | { 32 | throw new ProcessException("Expected an argument but reached the end of program args.", 33 | ProcessError.ExpectedProgramArgument, context.CommandLevel); 34 | } 35 | 36 | string valueToken = context.ProgramArguments.Dequeue(); 37 | 38 | if (!context.Parser.HandlesType()) 39 | { 40 | throw new ProcessException($"Failed to process program argument '{valueToken}' because the " 41 | + $"parser cannot handle the property type of '{typeof(TProperty).Name}'.", 42 | ProcessError.ParserUnhandledType, context.CommandLevel); 43 | } 44 | 45 | if (!context.Parser.TryParseAs(valueToken, out TProperty parsed)) 46 | { 47 | string message = _onParseErrorUseMessage != null 48 | ? _onParseErrorUseMessage(valueToken) 49 | : $"Failed to process program argument '{valueToken}' because it " 50 | + $"couldn't be parsed into a '{typeof(TProperty).Name}'."; 51 | 52 | throw new ProcessException(message, ProcessError.ParserInvalidValue, context.CommandLevel); 53 | } 54 | 55 | ProcessStageResult result = _onParsed?.Invoke(parsed); 56 | if (result == ProcessResult.End) 57 | { 58 | return ProcessResult.End; 59 | } 60 | 61 | PropertyInfo propertyInfo = ReflectionHelper.GetPropertyInfoFromExpression(_property); 62 | propertyInfo.SetValue(context.RunInfo, parsed); 63 | 64 | return ProcessResult.Continue; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Source/Processor/Stages/SequenceArgumentStage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace R5.RunInfoBuilder.Processor.Stages 8 | { 9 | internal class SequenceArgumentStage : Stage 10 | where TRunInfo : class 11 | { 12 | private Expression>> _listProperty { get; } 13 | private Func _onParsed { get; } 14 | private Func _onParseErrorUseMessage { get; } 15 | 16 | internal SequenceArgumentStage( 17 | Expression>> listProperty, 18 | Func onParsed, 19 | Func onParseErrorUseMessage) 20 | { 21 | _listProperty = listProperty; 22 | _onParsed = onParsed; 23 | _onParseErrorUseMessage = onParseErrorUseMessage; 24 | } 25 | 26 | internal override ProcessStageResult ProcessStage(ProcessContext context, 27 | Action> resetContextFunc) 28 | { 29 | if (!context.ProgramArguments.HasMore()) 30 | { 31 | throw new ProcessException("Expected a sequence of arguments but reached the end of program args.", 32 | ProcessError.ExpectedProgramArgument, context.CommandLevel); 33 | } 34 | 35 | PropertyInfo propertyInfo = ReflectionHelper.GetPropertyInfoFromExpression(_listProperty); 36 | 37 | // initialize list if null 38 | var list = (IList)propertyInfo.GetValue(context.RunInfo, null); 39 | if (list == null) 40 | { 41 | list = (IList)Activator.CreateInstance(propertyInfo.PropertyType); 42 | propertyInfo.SetValue(context.RunInfo, list); 43 | } 44 | 45 | // Iterate over proceeding program args, adding parseable items to list. 46 | // Ends when all program args are exhausted or when an option or subcommand 47 | // has been identified. 48 | while (context.ProgramArguments.HasMore()) 49 | { 50 | if (context.ProgramArguments.NextIsOption()) 51 | { 52 | return ProcessResult.Continue; 53 | } 54 | 55 | if (context.ProgramArguments.NextIsSubCommand()) 56 | { 57 | return ProcessResult.Continue; 58 | } 59 | 60 | string next = context.ProgramArguments.Dequeue(); 61 | 62 | if (!context.Parser.TryParseAs(next, out TListProperty parsed)) 63 | { 64 | string message = _onParseErrorUseMessage != null 65 | ? _onParseErrorUseMessage(next) 66 | : $"Failed to process program argument '{next}' because it " 67 | + $"couldn't be parsed into a '{typeof(TListProperty).Name}'."; 68 | 69 | throw new ProcessException(message, ProcessError.ParserInvalidValue, context.CommandLevel); 70 | } 71 | 72 | ProcessStageResult result = _onParsed?.Invoke(parsed); 73 | if (result == ProcessResult.End) 74 | { 75 | return ProcessResult.End; 76 | } 77 | 78 | list.Add(parsed); 79 | } 80 | 81 | return ProcessResult.End; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Source/Processor/Stages/SetArgumentStage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using System.Text; 7 | using R5.RunInfoBuilder.Processor.Models; 8 | 9 | namespace R5.RunInfoBuilder.Processor.Stages 10 | { 11 | internal class SetArgumentStage : Stage 12 | where TRunInfo : class 13 | { 14 | private Expression> _property { get; } 15 | private Dictionary _valueMap { get; } 16 | 17 | internal SetArgumentStage( 18 | Expression> property, 19 | List<(string, TProperty)> values) 20 | { 21 | _property = property; 22 | _valueMap = values.ToDictionary(v => v.Item1, v => v.Item2); 23 | } 24 | 25 | internal override ProcessStageResult ProcessStage(ProcessContext context, Action> resetContextFunc) 26 | { 27 | if (!context.ProgramArguments.HasMore()) 28 | { 29 | throw new ProcessException("Expected an argument but reached the end of program args.", 30 | ProcessError.ExpectedProgramArgument, context.CommandLevel); 31 | } 32 | 33 | string valueToken = context.ProgramArguments.Dequeue(); 34 | 35 | if (!_valueMap.ContainsKey(valueToken)) 36 | { 37 | throw new ProcessException($"'{valueToken}' is invalid for this set.", 38 | ProcessError.UnknownValue, context.CommandLevel); 39 | } 40 | 41 | PropertyInfo propertyInfo = ReflectionHelper.GetPropertyInfoFromExpression(_property); 42 | propertyInfo.SetValue(context.RunInfo, _valueMap[valueToken]); 43 | 44 | return ProcessResult.Continue; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Source/Processor/Stages/Stage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | 4 | namespace R5.RunInfoBuilder.Processor.Stages 5 | { 6 | internal abstract class Stage 7 | where TRunInfo : class 8 | { 9 | protected Stage() { } 10 | 11 | internal abstract ProcessStageResult ProcessStage(ProcessContext context, Action> resetContextFunc); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Source/Processor/Stages/SubCommandStage.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace R5.RunInfoBuilder.Processor.Stages 6 | { 7 | internal class SubCommandStage : Stage 8 | where TRunInfo : class 9 | { 10 | private Dictionary>, SubCommand)> _subCommandInfoMap { get; } 11 | 12 | internal SubCommandStage( 13 | Dictionary>, SubCommand)> subCommandInfoMap) 14 | { 15 | _subCommandInfoMap = subCommandInfoMap; 16 | } 17 | 18 | internal override ProcessStageResult ProcessStage(ProcessContext context, 19 | Action> resetContextFunc) 20 | { 21 | if (!context.ProgramArguments.HasMore()) 22 | { 23 | throw new ProcessException("Expected an argument but reached the end of program args.", 24 | ProcessError.ExpectedProgramArgument, context.CommandLevel); 25 | } 26 | 27 | string subCommand = context.ProgramArguments.Dequeue(); 28 | 29 | if (!_subCommandInfoMap.TryGetValue(subCommand, out (Queue>, SubCommand) subCommandInfo)) 30 | { 31 | throw new ProcessException($"'{subCommand}' is not a valid sub command.", 32 | ProcessError.InvalidSubCommand, context.CommandLevel); 33 | } 34 | 35 | (Queue> subCommandStages, SubCommand command) = subCommandInfo; 36 | 37 | ProcessStageResult onMatchedResult = command.OnMatched?.Invoke(context.RunInfo); 38 | if (onMatchedResult == ProcessResult.End) 39 | { 40 | return ProcessResult.End; 41 | } 42 | 43 | context.Stages.ExtendPipelineWith(subCommandStages); 44 | 45 | resetContextFunc(command); 46 | 47 | return ProcessResult.Continue; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/Processor/StagesFactory.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Processor.Stages; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace R5.RunInfoBuilder.Processor 8 | { 9 | internal class StagesFactory 10 | { 11 | internal Queue> Create( 12 | StackableCommand command, bool hasGlobalOptions, Action postBuildCallback) 13 | where TRunInfo : class 14 | { 15 | var pipeline = new Queue>(); 16 | 17 | if (command is Command rootCommand) 18 | { 19 | pipeline.Enqueue(rootCommand.ToStage()); 20 | } 21 | 22 | Queue> commonStages = BuildCommonPipelineStages(command, hasGlobalOptions); 23 | while (commonStages.Any()) 24 | { 25 | pipeline.Enqueue(commonStages.Dequeue()); 26 | } 27 | 28 | // recursively add subcommand pipelines 29 | if (command.SubCommands.Any()) 30 | { 31 | var subCommandInfoMap = new Dictionary>, SubCommand)>(); 32 | 33 | foreach (SubCommand subCommand in command.SubCommands) 34 | { 35 | Queue> subCommandPipeline = Create(subCommand, hasGlobalOptions, postBuildCallback); 36 | 37 | subCommandInfoMap.Add(subCommand.Key, (subCommandPipeline, subCommand)); 38 | } 39 | 40 | pipeline.Enqueue(new SubCommandStage(subCommandInfoMap)); 41 | } 42 | 43 | // EndProcessStage should ONLY be on a leaf stage node 44 | if (!command.SubCommands.Any()) 45 | { 46 | pipeline.Enqueue(new EndProcessStage(postBuildCallback)); 47 | } 48 | 49 | return pipeline; 50 | } 51 | 52 | internal Queue> Create( 53 | DefaultCommand defaultCommand, Action postBuildCallback) 54 | where TRunInfo : class 55 | { 56 | var pipeline = new Queue>(); 57 | 58 | pipeline.Enqueue(defaultCommand.ToStage()); 59 | 60 | Queue> commonStages = BuildCommonPipelineStages(defaultCommand, hasGlobalOptions: false); 61 | while (commonStages.Any()) 62 | { 63 | pipeline.Enqueue(commonStages.Dequeue()); 64 | } 65 | 66 | pipeline.Enqueue(new EndProcessStage(postBuildCallback)); 67 | 68 | return pipeline; 69 | } 70 | 71 | private Queue> BuildCommonPipelineStages( 72 | CommandBase command, bool hasGlobalOptions) 73 | where TRunInfo : class 74 | { 75 | var pipeline = new Queue>(); 76 | 77 | foreach (ArgumentBase argument in command.Arguments) 78 | { 79 | pipeline.Enqueue(argument.ToStage()); 80 | } 81 | 82 | if (hasGlobalOptions || command.Options.Any()) 83 | { 84 | pipeline.Enqueue(new OptionStage()); 85 | } 86 | 87 | return pipeline; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Source/RunInfoBuilder.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Parser; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace R5.RunInfoBuilder 6 | { 7 | /// 8 | /// Provides members for various configurations and a build method to start the parsing process. 9 | /// 10 | public class RunInfoBuilder 11 | { 12 | /// 13 | /// Used to parse program arguments. Can be configured to handle additional types. 14 | /// 15 | public ArgumentParser Parser { get; } 16 | 17 | /// 18 | /// Configures how the program's help menu is displayed. 19 | /// 20 | public HelpManager Help { get; } 21 | 22 | /// 23 | /// Stores command configurations used for parsing program arguments into run info objects. 24 | /// 25 | public CommandStore Commands { get; } 26 | 27 | /// 28 | /// Configures how the program's version information is displayed. 29 | /// 30 | public VersionManager Version { get; } 31 | 32 | /// 33 | /// Configures custom callbacks to be hooked into the build process. 34 | /// 35 | public BuildHooks Hooks { get; } 36 | 37 | /// 38 | /// Initializes a new instance of the RunInfoBuilder class. 39 | /// 40 | public RunInfoBuilder() 41 | { 42 | Parser = new ArgumentParser(); 43 | Help = new HelpManager(); 44 | Commands = new CommandStore(Parser, Help); 45 | Version = new VersionManager(); 46 | Hooks = new BuildHooks(); 47 | } 48 | 49 | /// 50 | /// Call to build run info objects from program arguments. 51 | /// 52 | /// Program arguments to be parsed. 53 | /// 54 | /// A run info object that's been configured in the Command Store 55 | /// and built by parsing the program arguments. 56 | /// 57 | public object Build(string[] args) 58 | { 59 | Hooks.OnStart?.Invoke(args); 60 | 61 | if ((args == null || !args.Any()) && Hooks.NullOrEmptyReturns != null) 62 | { 63 | return Hooks.NullOrEmptyReturns.Invoke(); 64 | } 65 | 66 | if (args == null) 67 | { 68 | throw new ArgumentNullException(nameof(args), "Program arguments must be provided."); 69 | } 70 | 71 | if (args.Any() && Help.IsTrigger(args.First())) 72 | { 73 | Help.Invoke(); 74 | return null; 75 | } 76 | 77 | if (args.Any() && Version.IsTrigger(args.First())) 78 | { 79 | Version.Invoke(); 80 | return null; 81 | } 82 | 83 | try 84 | { 85 | dynamic pipeline = Commands.ResolvePipelineFromArgs(args); 86 | var runInfo = pipeline.Process(); 87 | return runInfo; 88 | } 89 | catch (Exception ex) 90 | { 91 | // supress exceptions from bubbling to client 92 | if (Help.InvokeOnFail) 93 | { 94 | Help.Invoke(); 95 | 96 | if (Help.SuppressException) 97 | { 98 | return null; 99 | } 100 | 101 | throw; 102 | } 103 | 104 | if (ex is ProcessException processException) 105 | { 106 | throw; 107 | } 108 | 109 | // if not ProcessException, wrap and throw 110 | throw new ProcessException("Failed to process args. See the inner exception for more details.", 111 | innerException: ex); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Source/RunInfoBuilder.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | R5.RunInfoBuilder 6 | R5.RunInfoBuilder 7 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 8 | Debug;Release;Test 9 | Isaiah Lee 10 | 11 | A Command Line Parser utilizing object trees for command configurations. 12 | 2.0.0 13 | https://github.com/rushfive/RunInfoBuilder 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | latest 23 | 24 | 25 | 26 | latest 27 | 28 | 29 | 30 | latest 31 | 32 | 33 | -------------------------------------------------------------------------------- /Source/RunInfoBuilder.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | R5.RunInfoBuilder 5 | 2.0.0 6 | RunInfoBuilder 7 | Isaiah Lee 8 | Isaiah Lee 9 | https://github.com/rushfive/RunInfoBuilder 10 | false 11 | A Command Line Parser utilizing object trees for command configurations. 12 | Add several new features along with some useability updates. 13 | Copyright 2019 14 | command parser cli 15 | 16 | -------------------------------------------------------------------------------- /Source/Version/VersionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder 7 | { 8 | /// 9 | /// /// Provides methods to configure how the program's version information is displayed. 10 | /// 11 | public class VersionManager 12 | { 13 | private static readonly string[] _defaultTriggers = new string[] 14 | { 15 | "--version", "-v", "/version" 16 | }; 17 | 18 | private string _version { get; set; } 19 | private List _triggers { get; } 20 | 21 | internal VersionManager() 22 | { 23 | _version = "1.0.0"; 24 | _triggers = new List(_defaultTriggers); 25 | } 26 | 27 | /// 28 | /// Sets the program's version. 29 | /// 30 | /// String value representing the version. 31 | /// The VersionManager instance. 32 | public VersionManager Set(string version) 33 | { 34 | if (string.IsNullOrWhiteSpace(version)) 35 | { 36 | throw new ArgumentNullException(nameof(version), "Version must be provided."); 37 | } 38 | 39 | _version = version; 40 | return this; 41 | } 42 | 43 | /// 44 | /// Sets the list of keywords that will trigger the version to be displayed. 45 | /// 46 | /// List of triggers as params. 47 | /// The VersionManager instance. 48 | public VersionManager SetTriggers(params string[] triggers) 49 | { 50 | if (triggers == null || !triggers.Any()) 51 | { 52 | throw new ArgumentNullException(nameof(triggers), "Triggers must be provided."); 53 | } 54 | 55 | _triggers.Clear(); 56 | _triggers.AddRange(triggers); 57 | 58 | return this; 59 | } 60 | 61 | internal void Invoke() 62 | { 63 | Console.WriteLine(_version); 64 | } 65 | 66 | internal bool IsTrigger(string token) 67 | { 68 | return _triggers.Contains(token); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Test/FunctionalTests/FunctionalTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | R5.RunInfoBuilder.FunctionalTests 6 | R5.RunInfoBuilder.FunctionalTests 7 | Debug;Release;Test 8 | 9 | 10 | 11 | latest 12 | 13 | 14 | 15 | latest 16 | 17 | 18 | 19 | latest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Models/TestEnums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.FunctionalTests.Models 6 | { 7 | public enum TestEnum 8 | { 9 | ValueA, 10 | ValueB, 11 | ValueC 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Models/TestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.FunctionalTests.Models 6 | { 7 | public class TestException : Exception 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Models/TestRunInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace R5.RunInfoBuilder.FunctionalTests.Models 4 | { 5 | public class TestRunInfo 6 | { 7 | public List StringList1 { get; set; } 8 | public List IntList1 { get; set; } 9 | public List IntList2 { get; set; } 10 | public List DoubleList1 { get; set; } 11 | public List UnwritableStringList { get; } 12 | public string String1 { get; set; } 13 | public string String2 { get; set; } 14 | public string String3 { get; set; } 15 | public bool Bool1 { get; set; } 16 | public bool Bool2 { get; set; } 17 | public bool Bool3 { get; set; } 18 | public int Int1 { get; set; } 19 | public int Int2 { get; set; } 20 | public int Int3 { get; set; } 21 | public double Double1 { get; set; } 22 | public bool UnwritableBool { get; } 23 | public TestCustomType CustomType { get; set; } 24 | public bool BoolFromCustomArg1 { get; set; } 25 | public bool BoolFromCustomArg2 { get; set; } 26 | public bool BoolFromCustomArg3 { get; set; } 27 | public TestEnum? NullableEnum { get; set; } 28 | } 29 | 30 | public class TestCustomType 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/ComplexScenarios/ComplexScenarioBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.ComplexScenarios 7 | { 8 | public static class ComplexScenarioBuilderFactory 9 | { 10 | public static RunInfoBuilder GetBuilder() 11 | { 12 | var builder = new RunInfoBuilder(); 13 | 14 | builder.Commands 15 | .AddDefault(new DefaultCommand 16 | { 17 | Options = Options() 18 | }) 19 | .Add(Commands.SingleLevel.CustomArgToSequence()) 20 | .Add(Commands.MultiLevel.ComplexNestedCommand()); 21 | 22 | return builder; 23 | } 24 | 25 | private static List> Options() 26 | { 27 | return new List> 28 | { 29 | new Option 30 | { 31 | Key = "string | s", 32 | Property = ri => ri.String1 33 | }, 34 | new Option 35 | { 36 | Key = "int | i", 37 | Property = ri => ri.Int1, 38 | OnParsed = (int val) => 39 | { 40 | if (val < 100) 41 | { 42 | throw new TestException(); 43 | } 44 | return ProcessResult.Continue; 45 | } 46 | }, 47 | new Option 48 | { 49 | Key = "bool1 | a", 50 | Property = ri => ri.Bool1 51 | }, 52 | new Option 53 | { 54 | Key = "bool2 | b", 55 | Property = ri => ri.Bool2 56 | } 57 | }; 58 | } 59 | 60 | private static class Commands 61 | { 62 | public static class SingleLevel 63 | { 64 | // custom arg followed by sequence 65 | public static Command CustomArgToSequence() 66 | { 67 | return new Command 68 | { 69 | Key = "CustomToSequence", 70 | Options = Options(), 71 | Arguments = 72 | { 73 | new CustomArgument 74 | { 75 | Count = 2, 76 | HelpToken = "custom_help_token", 77 | Handler = context => 78 | { 79 | context.RunInfo.BoolFromCustomArg1 = true; 80 | return ProcessResult.Continue; 81 | } 82 | }, 83 | new SequenceArgument 84 | { 85 | ListProperty = ri => ri.IntList1 86 | } 87 | } 88 | }; 89 | } 90 | } 91 | 92 | public static class MultiLevel 93 | { 94 | public static Command ComplexNestedCommand() 95 | { 96 | return new Command 97 | { 98 | Key = "ComplexCommand", 99 | Options = Options(), 100 | Arguments = 101 | { 102 | new SetArgument 103 | { 104 | Property = ri => ri.Int1, 105 | Values = 106 | { 107 | ("one", 1), ("two", 2) 108 | } 109 | }, 110 | new PropertyArgument 111 | { 112 | Property = ri => ri.Int2 113 | }, 114 | new CustomArgument 115 | { 116 | Count = 2, 117 | HelpToken = "", 118 | Handler = context => 119 | { 120 | int first = int.Parse(context.ProgramArguments[0]); 121 | int second = int.Parse(context.ProgramArguments[1]); 122 | context.RunInfo.Int3 = first + second; 123 | return ProcessResult.Continue; 124 | } 125 | } 126 | }, 127 | SubCommands = 128 | { 129 | new SubCommand 130 | { 131 | Key = "StringSubCommand", 132 | Options = Options(), 133 | Arguments = 134 | { 135 | new PropertyArgument 136 | { 137 | Property = ri => ri.String1 138 | }, 139 | new SetArgument 140 | { 141 | Property = ri => ri.String2, 142 | Values = 143 | { 144 | ( "one", "one" ), ("two", "two") 145 | } 146 | }, 147 | new CustomArgument 148 | { 149 | Count = 2, 150 | HelpToken = "", 151 | Handler = context => 152 | { 153 | context.RunInfo.String3 = 154 | context.ProgramArguments[0] + context.ProgramArguments[1]; 155 | return ProcessResult.Continue; 156 | } 157 | } 158 | } 159 | }, 160 | new SubCommand 161 | { 162 | Key = "DoubleSubCommand", 163 | Arguments = 164 | { 165 | new PropertyArgument 166 | { 167 | Property = ri => ri.Double1 168 | }, 169 | new SequenceArgument 170 | { 171 | ListProperty = ri => ri.DoubleList1, 172 | OnParsed = val => 173 | { 174 | if (val < 9.9) 175 | { 176 | return ProcessResult.End; 177 | } 178 | return ProcessResult.Continue; 179 | } 180 | } 181 | } 182 | } 183 | } 184 | }; 185 | } 186 | } 187 | 188 | 189 | 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/ComplexScenarios/ComplexScenarioTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.ComplexScenarios 8 | { 9 | // TODO: better name 10 | public class ComplexScenarioTests 11 | { 12 | private static RunInfoBuilder GetBuilder() => ComplexScenarioBuilderFactory.GetBuilder(); 13 | 14 | public class SingleLevel 15 | { 16 | [Fact] 17 | public void FirstTest_BindsCorrectly() 18 | { 19 | RunInfoBuilder builder = GetBuilder(); 20 | 21 | var args = new string[] 22 | { 23 | "CustomToSequence", "custom1", "custom2", "1", "2", "3", "-s=hello", "--int", "101", "-ab" 24 | }; 25 | 26 | TestRunInfo result = (TestRunInfo)builder.Build(args); 27 | 28 | Assert.Equal("hello", result.String1); 29 | Assert.Equal(101, result.Int1); 30 | Assert.True(result.Bool1); 31 | Assert.True(result.Bool2); 32 | Assert.True(result.BoolFromCustomArg1); 33 | Assert.Equal(3, result.IntList1.Count); 34 | Assert.Equal(1, result.IntList1[0]); 35 | Assert.Equal(2, result.IntList1[1]); 36 | Assert.Equal(3, result.IntList1[2]); 37 | } 38 | } 39 | 40 | public class MultiLevel 41 | { 42 | [Fact] 43 | public void StringSubCommand_BindsCorrectly() 44 | { 45 | RunInfoBuilder builder = GetBuilder(); 46 | 47 | var args = new string[] 48 | { 49 | "ComplexCommand", "one", "33", "5", "6", "-s", "short_string", "--int=100", "--bool1", 50 | "StringSubCommand", "short_string_from_sub", "two", "first", "last", "--bool2" 51 | }; 52 | 53 | TestRunInfo result = (TestRunInfo)builder.Build(args); 54 | 55 | //Assert.Equal(2, result.Int1); 56 | Assert.Equal(33, result.Int2); 57 | Assert.Equal(11, result.Int3); 58 | //Assert.Equal("short_string", result.String1); 59 | Assert.Equal(100, result.Int1); // should override the fisrt arg "one" 60 | Assert.True(result.Bool1); 61 | Assert.Equal("short_string_from_sub", result.String1); // overrides top level command 62 | Assert.Equal("two", result.String2); 63 | Assert.Equal("firstlast", result.String3); 64 | Assert.True(result.Bool2); 65 | } 66 | 67 | [Fact] 68 | public void DoubleSubCommand_BindsCorrectly() 69 | { 70 | RunInfoBuilder builder = GetBuilder(); 71 | 72 | var args = new string[] 73 | { 74 | "ComplexCommand", "one", "33", "5", "6", "-s", "short_string", "--int=100", "--bool1", 75 | "DoubleSubCommand", "3.7", "10.1", "10.2", "10.3", "9", "8", "10.5" 76 | }; 77 | 78 | TestRunInfo result = (TestRunInfo)builder.Build(args); 79 | 80 | //Assert.Equal(2, result.Int1); 81 | Assert.Equal(33, result.Int2); 82 | Assert.Equal(11, result.Int3); 83 | //Assert.Equal("short_string", result.String1); 84 | Assert.Equal(100, result.Int1); // should override the fisrt arg "one" 85 | Assert.True(result.Bool1); 86 | 87 | Assert.Equal(3.7, result.Double1); 88 | Assert.Equal(3, result.DoubleList1.Count); 89 | Assert.Equal(10.1, result.DoubleList1[0]); 90 | Assert.Equal(10.2, result.DoubleList1[1]); 91 | Assert.Equal(10.3, result.DoubleList1[2]); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Hooks/NullOrEmptyReturnsTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Hooks 8 | { 9 | public class NullOrEmptyReturnsTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | [Fact] 17 | public void NullCallback_Throws() 18 | { 19 | Action testCode = () => 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Hooks.ArgsNullOrEmptyReturns(null); 24 | }; 25 | 26 | Exception exception = Record.Exception(testCode); 27 | 28 | Assert.IsType(exception); 29 | } 30 | 31 | [Fact] 32 | public void Invoked_IfSet_AndArgsNull() 33 | { 34 | RunInfoBuilder builder = GetBuilder(); 35 | 36 | builder.Hooks.ArgsNullOrEmptyReturns(() => 100); 37 | 38 | var result = builder.Build(null); 39 | 40 | Assert.Equal(100, result); 41 | } 42 | 43 | [Fact] 44 | public void Invoked_IfSet_AndArgsEmpty() 45 | { 46 | RunInfoBuilder builder = GetBuilder(); 47 | 48 | builder.Hooks.ArgsNullOrEmptyReturns(() => 100); 49 | 50 | var result = builder.Build(new string[] { }); 51 | 52 | Assert.Equal(100, result); 53 | } 54 | 55 | [Fact] 56 | public void OnlyInvokes_IfSet_AndArgsNullOrEmpty() 57 | { 58 | RunInfoBuilder builder = GetBuilder(); 59 | 60 | builder.Hooks.ArgsNullOrEmptyReturns(() => throw new TestException()); 61 | 62 | builder.Commands.Add(new Command 63 | { 64 | Key = "command" 65 | }); 66 | 67 | Exception exception = Record.Exception(() => 68 | { 69 | builder.Build(new string[] { "command" }); 70 | }); 71 | 72 | Assert.Null(exception); 73 | 74 | exception = Record.Exception(() => 75 | { 76 | builder.Build(null); 77 | }); 78 | 79 | Assert.NotNull(exception); 80 | Assert.IsType(exception); 81 | 82 | exception = Record.Exception(() => 83 | { 84 | builder.Build(new string[] { }); 85 | }); 86 | 87 | Assert.NotNull(exception); 88 | Assert.IsType(exception); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Hooks/OnStartTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Hooks 8 | { 9 | public class OnStartTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | [Fact] 17 | public void NullCallback_Throws() 18 | { 19 | Action testCode = () => 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Hooks.OnStartBuild(null); 24 | }; 25 | 26 | Exception exception = Record.Exception(testCode); 27 | 28 | Assert.IsType(exception); 29 | } 30 | 31 | [Fact] 32 | public void If_OnStartSet_Invokes() 33 | { 34 | RunInfoBuilder builder = GetBuilder(); 35 | 36 | bool setFromCallback = false; 37 | 38 | builder.Hooks.OnStartBuild(args => 39 | { 40 | setFromCallback = true; 41 | }); 42 | 43 | builder.Commands.AddDefault(new DefaultCommand 44 | { 45 | Options = 46 | { 47 | new Option 48 | { 49 | Key = "bool", 50 | Property = ri => ri.Bool1 51 | } 52 | } 53 | }); 54 | 55 | builder.Build(new string[] { "--bool" }); 56 | 57 | Assert.True(setFromCallback); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Parser/NullableEnumParseTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Parser 8 | { 9 | public class NullableEnumParseTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | var builder = new RunInfoBuilder(); 14 | 15 | builder.Commands.AddDefault(new DefaultCommand 16 | { 17 | Options = 18 | { 19 | new Option 20 | { 21 | Key = "enum", 22 | Property = ri => ri.NullableEnum 23 | } 24 | } 25 | }); 26 | 27 | return builder; 28 | } 29 | 30 | [Fact] 31 | public void EmptyStringArgument_SetsAsNull() 32 | { 33 | RunInfoBuilder builder = GetBuilder(); 34 | 35 | var result = (TestRunInfo)builder.Build(new string[] { "--enum", "" }); 36 | 37 | Assert.Null(result.NullableEnum); 38 | } 39 | 40 | [Fact] 41 | public void ValidEnumArgument_SetsCorrectValue() 42 | { 43 | RunInfoBuilder builder = GetBuilder(); 44 | 45 | var result = (TestRunInfo)builder.Build(new string[] { "--enum", "ValueB" }); 46 | 47 | Assert.Equal(TestEnum.ValueB, result.NullableEnum); 48 | } 49 | 50 | [Fact] 51 | public void InvalidEnumArgument_Failed() 52 | { 53 | Action testCode = () => 54 | { 55 | RunInfoBuilder builder = GetBuilder(); 56 | 57 | var result = (TestRunInfo)builder.Build(new string[] { "--enum", "InvalidEnumValue" }); 58 | }; 59 | 60 | Exception exception = Record.Exception(testCode); 61 | 62 | var processException = exception as ProcessException; 63 | 64 | Assert.NotNull(processException); 65 | Assert.Equal(ProcessError.ParserInvalidValue, processException.ErrorType); 66 | Assert.Equal(0, processException.CommandLevel); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/Command/CommandTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.Command 8 | { 9 | public class CommandTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class InSingleCommand 17 | { 18 | public class PostBuildCallback 19 | { 20 | [Fact] 21 | public void OnSuccessfulBuild_Invokes() 22 | { 23 | RunInfoBuilder builder = GetBuilder(); 24 | 25 | builder.Commands.Add( 26 | new Command 27 | { 28 | Key = "command", 29 | Options = 30 | { 31 | new Option 32 | { 33 | Key = "bool", 34 | Property = ri => ri.Bool1 35 | } 36 | } 37 | }, 38 | runInfo => 39 | { 40 | Assert.True(runInfo.Bool1); 41 | }); 42 | 43 | builder.Build(new string[] { "command", "--bool" }); 44 | } 45 | } 46 | 47 | public class OnMatchedCallback 48 | { 49 | [Fact] 50 | public void NotNull_Invokes() 51 | { 52 | RunInfoBuilder builder = GetBuilder(); 53 | 54 | bool flag = false; 55 | 56 | builder.Commands.Add( 57 | new Command 58 | { 59 | Key = "command", 60 | OnMatched = runInfo => 61 | { 62 | flag = true; 63 | return ProcessResult.Continue; 64 | } 65 | }); 66 | 67 | Assert.False(flag); 68 | builder.Build(new string[] { "command" }); 69 | Assert.True(flag); 70 | } 71 | 72 | [Fact] 73 | public void EndResult_StopsFurtherProcessing() 74 | { 75 | RunInfoBuilder builder = GetBuilder(); 76 | 77 | builder.Commands.Add( 78 | new Command 79 | { 80 | Key = "command", 81 | OnMatched = ri => ProcessResult.End, 82 | Options = 83 | { 84 | new Option 85 | { 86 | Key = "bool", 87 | Property = ri => ri.Bool1 88 | } 89 | } 90 | }); 91 | 92 | var runInfo = builder.Build(new string[] { "command", "--bool" }) as TestRunInfo; 93 | Assert.False(runInfo?.Bool1); 94 | } 95 | 96 | [Fact] 97 | public void ContinueResult_CorrectlySetsFlag() 98 | { 99 | RunInfoBuilder builder = GetBuilder(); 100 | 101 | builder.Commands.Add( 102 | new Command 103 | { 104 | Key = "command", 105 | OnMatched = ri => 106 | { 107 | ri.String1 = "SetFromOnMatched"; 108 | 109 | return ProcessResult.Continue; 110 | }, 111 | Options = 112 | { 113 | new Option 114 | { 115 | Key = "bool", 116 | Property = ri => ri.Bool1 117 | } 118 | } 119 | }); 120 | 121 | var runInfo = builder.Build(new string[] { "command", "--bool" }) as TestRunInfo; 122 | Assert.True(runInfo?.Bool1); 123 | Assert.Equal("SetFromOnMatched", runInfo?.String1); 124 | } 125 | } 126 | } 127 | 128 | public class InNestedCommand 129 | { 130 | public class PostBuildCallback 131 | { 132 | [Fact] 133 | public void OnSuccessfulBuild_Invokes() 134 | { 135 | RunInfoBuilder builder = GetBuilder(); 136 | 137 | builder.Commands.Add( 138 | new Command 139 | { 140 | Key = "command", 141 | Options = 142 | { 143 | new Option 144 | { 145 | Key = "bool", 146 | Property = ri => ri.Bool1 147 | } 148 | }, 149 | SubCommands = 150 | { 151 | new SubCommand 152 | { 153 | Key = "subcommand", 154 | Arguments = 155 | { 156 | new PropertyArgument 157 | { 158 | Property = ri => ri.Int1 159 | } 160 | } 161 | } 162 | } 163 | }, 164 | runInfo => 165 | { 166 | Assert.True(runInfo.Bool1); 167 | Assert.Equal(99, runInfo.Int1); 168 | }); 169 | 170 | builder.Build(new string[] { "command", "--bool", "subcommand", "99" }); 171 | } 172 | } 173 | 174 | public class OnMatchedCallback 175 | { 176 | [Fact] 177 | public void NotNull_Invokes() 178 | { 179 | RunInfoBuilder builder = GetBuilder(); 180 | 181 | bool flag = false; 182 | 183 | builder.Commands.Add( 184 | new Command 185 | { 186 | Key = "command", 187 | SubCommands = 188 | { 189 | new SubCommand 190 | { 191 | Key = "subcommand", 192 | OnMatched = runInfo => 193 | { 194 | flag = true; 195 | return ProcessResult.Continue; 196 | } 197 | } 198 | } 199 | }); 200 | 201 | Assert.False(flag); 202 | builder.Build(new string[] { "command", "subcommand" }); 203 | Assert.True(flag); 204 | } 205 | 206 | [Fact] 207 | public void EndResult_StopsFurtherProcessing() 208 | { 209 | RunInfoBuilder builder = GetBuilder(); 210 | 211 | builder.Commands.Add( 212 | new Command 213 | { 214 | Key = "command", 215 | SubCommands = 216 | { 217 | new SubCommand 218 | { 219 | Key = "subcommand", 220 | OnMatched = ri => ProcessResult.End, 221 | Options = 222 | { 223 | new Option 224 | { 225 | Key = "bool", 226 | Property = ri => ri.Bool1 227 | } 228 | } 229 | } 230 | } 231 | }); 232 | 233 | var runInfo = builder.Build(new string[] { "command", "subcommand", "--bool" }) as TestRunInfo; 234 | Assert.False(runInfo?.Bool1); 235 | } 236 | 237 | [Fact] 238 | public void ContinueResult_CorrectlySetsFlag() 239 | { 240 | RunInfoBuilder builder = GetBuilder(); 241 | 242 | builder.Commands.Add( 243 | new Command 244 | { 245 | Key = "command", 246 | SubCommands = 247 | { 248 | new SubCommand 249 | { 250 | Key = "subcommand", 251 | OnMatched = ri => 252 | { 253 | ri.String1 = "SetFromOnMatched"; 254 | 255 | return ProcessResult.Continue; 256 | }, 257 | Options = 258 | { 259 | new Option 260 | { 261 | Key = "bool", 262 | Property = ri => ri.Bool1 263 | } 264 | } 265 | } 266 | } 267 | }); 268 | 269 | var runInfo = builder.Build(new string[] { "command", "subcommand", "--bool" }) as TestRunInfo; 270 | Assert.True(runInfo?.Bool1); 271 | Assert.Equal("SetFromOnMatched", runInfo?.String1); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/Command/DefaultCommandTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.Command 8 | { 9 | public class DefaultCommandTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class Options 17 | { 18 | [Fact] 19 | public void Processes_Successfully() 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Commands.AddDefault(new DefaultCommand 24 | { 25 | Options = 26 | { 27 | new Option 28 | { 29 | Key = "bool | b", 30 | Property = ri => ri.Bool1 31 | }, 32 | new Option 33 | { 34 | Key = "int | i", 35 | Property = ri => ri.Int1 36 | } 37 | } 38 | }); 39 | 40 | assertFromArgs(new string[] { "--bool", "--int=99" }); 41 | assertFromArgs(new string[] { "--bool=true", "--int=99" }); 42 | assertFromArgs(new string[] { "-b", "true", "-i=99" }); 43 | 44 | void assertFromArgs(string[] args) 45 | { 46 | var result = (TestRunInfo)builder.Build(args); 47 | Assert.True(result.Bool1); 48 | Assert.Equal(99, result.Int1); 49 | } 50 | } 51 | 52 | [Fact] 53 | public void Processes_Stacked_Successfully() 54 | { 55 | RunInfoBuilder builder = GetBuilder(); 56 | 57 | builder.Commands.AddDefault(new DefaultCommand 58 | { 59 | Options = 60 | { 61 | new Option 62 | { 63 | Key = "bool1 | a", 64 | Property = ri => ri.Bool1 65 | }, 66 | new Option 67 | { 68 | Key = "bool2 | b", 69 | Property = ri => ri.Bool2 70 | }, 71 | new Option 72 | { 73 | Key = "bool3 | c", 74 | Property = ri => ri.Bool3 75 | }, 76 | } 77 | }); 78 | 79 | assertFromArgs(new string[] { "-abc" }); 80 | assertFromArgs(new string[] { "-abc", "true" }); 81 | assertFromArgs(new string[] { "-abc=true" }); 82 | 83 | void assertFromArgs(string[] args) 84 | { 85 | var result = (TestRunInfo)builder.Build(args); 86 | Assert.True(result.Bool1); 87 | Assert.True(result.Bool2); 88 | Assert.True(result.Bool3); 89 | 90 | } 91 | } 92 | } 93 | 94 | public class Arguments 95 | { 96 | [Fact] 97 | public void PropertyArgument_Success() 98 | { 99 | RunInfoBuilder builder = GetBuilder(); 100 | 101 | builder.Commands.AddDefault(new DefaultCommand 102 | { 103 | Arguments = 104 | { 105 | new PropertyArgument 106 | { 107 | Property = ri => ri.Int1 108 | } 109 | } 110 | }); 111 | 112 | var result = (TestRunInfo)builder.Build(new string[] { "99" }); 113 | Assert.Equal(99, result.Int1); 114 | } 115 | 116 | [Fact] 117 | public void SequenceArgument_Success() 118 | { 119 | RunInfoBuilder builder = GetBuilder(); 120 | 121 | builder.Commands.AddDefault(new DefaultCommand 122 | { 123 | Arguments = 124 | { 125 | new SequenceArgument 126 | { 127 | ListProperty = ri => ri.IntList1 128 | } 129 | } 130 | }); 131 | 132 | var result = (TestRunInfo)builder.Build(new string[] { "1", "2", "3" }); 133 | 134 | Assert.Equal(3, result.IntList1.Count); 135 | Assert.Equal(1, result.IntList1[0]); 136 | Assert.Equal(2, result.IntList1[1]); 137 | Assert.Equal(3, result.IntList1[2]); 138 | } 139 | 140 | [Fact] 141 | public void CustomArgument_Success() 142 | { 143 | RunInfoBuilder builder = GetBuilder(); 144 | 145 | builder.Commands.AddDefault(new DefaultCommand 146 | { 147 | Arguments = 148 | { 149 | new CustomArgument 150 | { 151 | Count = 3, 152 | Handler = context => 153 | { 154 | Assert.Equal(3, context.ProgramArguments.Count); 155 | Assert.Equal("1", context.ProgramArguments[0]); 156 | Assert.Equal("abc", context.ProgramArguments[1]); 157 | Assert.Equal("!!!", context.ProgramArguments[2]); 158 | return ProcessResult.Continue; 159 | }, 160 | HelpToken = "helptoken" 161 | } 162 | } 163 | }); 164 | 165 | builder.Build(new string[] { "1", "abc", "!!!" }); 166 | } 167 | } 168 | 169 | public class OnMatchedCallback 170 | { 171 | [Fact] 172 | public void NotNull_Invokes() 173 | { 174 | RunInfoBuilder builder = GetBuilder(); 175 | 176 | bool flag = false; 177 | 178 | builder.Commands.AddDefault( 179 | new DefaultCommand 180 | { 181 | OnMatched = runInfo => 182 | { 183 | flag = true; 184 | return ProcessResult.Continue; 185 | } 186 | }); 187 | 188 | Assert.False(flag); 189 | builder.Build(new string[] { }); 190 | Assert.True(flag); 191 | } 192 | 193 | [Fact] 194 | public void EndResult_StopsFurtherProcessing() 195 | { 196 | RunInfoBuilder builder = GetBuilder(); 197 | 198 | builder.Commands.AddDefault( 199 | new DefaultCommand 200 | { 201 | OnMatched = ri => ProcessResult.End, 202 | Options = 203 | { 204 | new Option 205 | { 206 | Key = "bool", 207 | Property = ri => ri.Bool1 208 | } 209 | } 210 | }); 211 | 212 | var runInfo = builder.Build(new string[] { "--bool" }) as TestRunInfo; 213 | Assert.False(runInfo?.Bool1); 214 | } 215 | 216 | [Fact] 217 | public void ContinueResult_StopsFurtherProcessing() 218 | { 219 | RunInfoBuilder builder = GetBuilder(); 220 | 221 | builder.Commands.AddDefault( 222 | new DefaultCommand 223 | { 224 | OnMatched = ri => 225 | { 226 | ri.String1 = "SetFromOnMatched"; 227 | 228 | return ProcessResult.Continue; 229 | }, 230 | Options = 231 | { 232 | new Option 233 | { 234 | Key = "bool", 235 | Property = ri => ri.Bool1 236 | } 237 | } 238 | }); 239 | 240 | var runInfo = builder.Build(new string[] { "--bool" }) as TestRunInfo; 241 | Assert.True(runInfo?.Bool1); 242 | Assert.Equal("SetFromOnMatched", runInfo?.String1); 243 | } 244 | } 245 | 246 | public class PostBuildCallback 247 | { 248 | [Fact] 249 | public void OnSuccessfulBuild_Invokes() 250 | { 251 | RunInfoBuilder builder = GetBuilder(); 252 | 253 | builder.Commands.AddDefault( 254 | new DefaultCommand 255 | { 256 | Options = 257 | { 258 | new Option 259 | { 260 | Key = "bool", 261 | Property = ri => ri.Bool1 262 | } 263 | } 264 | }, 265 | runInfo => 266 | { 267 | Assert.True(runInfo.Bool1); 268 | }); 269 | 270 | builder.Build(new string[] { "--bool" }); 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/Command/SubCommandTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.Command 8 | { 9 | public class SubCommandTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class RootCommandLevel 17 | { 18 | [Fact] 19 | public void SubCommandsConfigured_NoMoreArgs_Throws() 20 | { 21 | Action testCode = () => 22 | { 23 | RunInfoBuilder builder = GetBuilder(); 24 | 25 | builder.Commands.Add(new Command 26 | { 27 | Key = "command", 28 | SubCommands = 29 | { 30 | new SubCommand 31 | { 32 | Key = "subcommand" 33 | } 34 | } 35 | }); 36 | 37 | builder.Build(new string[] { "command" }); 38 | }; 39 | 40 | Exception exception = Record.Exception(testCode); 41 | 42 | var processException = exception as ProcessException; 43 | 44 | Assert.NotNull(processException); 45 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 46 | Assert.Equal(0, processException.CommandLevel); 47 | } 48 | 49 | [Fact] 50 | public void SubCommandsConfigured_NextArg_DoestMatch_Throws() 51 | { 52 | Action testCode = () => 53 | { 54 | RunInfoBuilder builder = GetBuilder(); 55 | 56 | builder.Commands.Add(new Command 57 | { 58 | Key = "command", 59 | SubCommands = 60 | { 61 | new SubCommand 62 | { 63 | Key = "subcommand" 64 | } 65 | } 66 | }); 67 | 68 | builder.Build(new string[] { "command", "invalid_match" }); 69 | }; 70 | 71 | Exception exception = Record.Exception(testCode); 72 | 73 | var processException = exception as ProcessException; 74 | 75 | Assert.NotNull(processException); 76 | Assert.Equal(ProcessError.InvalidSubCommand, processException.ErrorType); 77 | Assert.Equal(0, processException.CommandLevel); 78 | } 79 | } 80 | 81 | public class BelowRootCommandLevel 82 | { 83 | [Fact] 84 | public void SubCommandsConfigured_NoMoreArgs_Throws() 85 | { 86 | Action testCode = () => 87 | { 88 | RunInfoBuilder builder = GetBuilder(); 89 | 90 | builder.Commands.Add(new Command 91 | { 92 | Key = "command", 93 | SubCommands = 94 | { 95 | new SubCommand 96 | { 97 | Key = "sub1", 98 | SubCommands = 99 | { 100 | new SubCommand 101 | { 102 | Key = "sub2" 103 | } 104 | } 105 | } 106 | } 107 | }); 108 | 109 | builder.Build(new string[] { "command", "sub1" }); 110 | }; 111 | 112 | Exception exception = Record.Exception(testCode); 113 | 114 | var processException = exception as ProcessException; 115 | 116 | Assert.NotNull(processException); 117 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 118 | Assert.Equal(1, processException.CommandLevel); 119 | } 120 | 121 | [Fact] 122 | public void SubCommandsConfigured_NextArg_DoestMatch_Throws() 123 | { 124 | Action testCode = () => 125 | { 126 | RunInfoBuilder builder = GetBuilder(); 127 | 128 | builder.Commands.Add(new Command 129 | { 130 | Key = "command", 131 | SubCommands = 132 | { 133 | new SubCommand 134 | { 135 | Key = "sub1", 136 | SubCommands = 137 | { 138 | new SubCommand 139 | { 140 | Key = "sub2" 141 | } 142 | } 143 | } 144 | } 145 | }); 146 | 147 | builder.Build(new string[] { "command", "sub1", "invalid_match" }); 148 | }; 149 | 150 | Exception exception = Record.Exception(testCode); 151 | 152 | var processException = exception as ProcessException; 153 | 154 | Assert.NotNull(processException); 155 | Assert.Equal(ProcessError.InvalidSubCommand, processException.ErrorType); 156 | Assert.Equal(1, processException.CommandLevel); 157 | } 158 | } 159 | 160 | 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/CustomArgument/FailTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.CustomArgument 8 | { 9 | public class CustomArgumentFailTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | [Fact] 17 | public void NotEnoughProgramArguments_ForHandlerContext_Throws() 18 | { 19 | Action testCode = () => 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Commands.Add(new Command 24 | { 25 | Key = "command", 26 | Arguments = 27 | { 28 | new CustomArgument 29 | { 30 | Count = 2, 31 | Handler = context => ProcessResult.Continue, 32 | HelpToken = "helptoken" 33 | } 34 | } 35 | }); 36 | 37 | builder.Build(new string[] { "command", "one" }); 38 | }; 39 | 40 | Exception exception = Record.Exception(testCode); 41 | 42 | var processException = exception as ProcessException; 43 | 44 | Assert.NotNull(processException); 45 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 46 | Assert.Equal(0, processException.CommandLevel); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/CustomArgument/SuccessTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.CustomArgument 8 | { 9 | public class CustomArgumentSuccessTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | [Fact] 17 | public void CustomHandlerContext_ContainsCorrect_ProgramArguments() 18 | { 19 | RunInfoBuilder builder = GetBuilder(); 20 | 21 | builder.Commands.Add(new Command 22 | { 23 | Key = "command", 24 | Arguments = 25 | { 26 | new PropertyArgument 27 | { 28 | Property = ri => ri.Bool1 29 | }, 30 | new CustomArgument 31 | { 32 | Count = 3, 33 | Handler = context => 34 | { 35 | Assert.Equal(3, context.ProgramArguments.Count); 36 | Assert.Equal("1", context.ProgramArguments[0]); 37 | Assert.Equal("2", context.ProgramArguments[1]); 38 | Assert.Equal("3", context.ProgramArguments[2]); 39 | context.RunInfo.Int1 = 10; 40 | return ProcessResult.Continue; 41 | }, 42 | HelpToken = "helptoken" 43 | }, 44 | new PropertyArgument 45 | { 46 | Property = ri => ri.Bool2 47 | } 48 | } 49 | }); 50 | 51 | var runInfo = builder.Build(new string[] { "command", "true", "1", "2", "3", "true" }); 52 | 53 | var testRunInfo = runInfo as TestRunInfo; 54 | Assert.NotNull(testRunInfo); 55 | 56 | Assert.True(testRunInfo.Bool1); 57 | Assert.True(testRunInfo.Bool2); 58 | Assert.Equal(10, testRunInfo.Int1); 59 | } 60 | 61 | [Fact] 62 | public void CustomHandler_ReturnsEndResult_StopsFurtherProcessing() 63 | { 64 | RunInfoBuilder builder = GetBuilder(); 65 | 66 | builder.Commands.Add(new Command 67 | { 68 | Key = "command", 69 | Arguments = 70 | { 71 | new CustomArgument 72 | { 73 | Count = 2, 74 | Handler = context => ProcessResult.End, 75 | HelpToken = "helptoken" 76 | }, 77 | new PropertyArgument 78 | { 79 | Property = ri => ri.Bool3 80 | }, 81 | new CustomArgument 82 | { 83 | Count = 1, 84 | Handler = context => throw new Exception(), 85 | HelpToken = "helptoken" 86 | } 87 | } 88 | }); 89 | 90 | var runInfo = builder.Build(new string[] { "command", "1", "1", "true" }); 91 | 92 | var testRunInfo = runInfo as TestRunInfo; 93 | Assert.NotNull(testRunInfo); 94 | 95 | Assert.False(testRunInfo.Bool3); 96 | } 97 | 98 | [Fact] 99 | public void TwoSequential_CustomArguments_ProcessesCorrectly() 100 | { 101 | RunInfoBuilder builder = GetBuilder(); 102 | 103 | builder.Commands.Add(new Command 104 | { 105 | Key = "command", 106 | Arguments = 107 | { 108 | new CustomArgument 109 | { 110 | Count = 2, 111 | Handler = context => 112 | { 113 | Assert.Equal(2, context.ProgramArguments.Count); 114 | Assert.Equal("1", context.ProgramArguments[0]); 115 | Assert.Equal("2", context.ProgramArguments[1]); 116 | context.RunInfo.Int1 = 10; 117 | return ProcessResult.Continue; 118 | }, 119 | HelpToken = "helptoken" 120 | }, 121 | new CustomArgument 122 | { 123 | Count = 3, 124 | Handler = context => 125 | { 126 | Assert.Equal(3, context.ProgramArguments.Count); 127 | Assert.Equal("3", context.ProgramArguments[0]); 128 | Assert.Equal("4", context.ProgramArguments[1]); 129 | Assert.Equal("5", context.ProgramArguments[2]); 130 | context.RunInfo.Int2 = 20; 131 | return ProcessResult.Continue; 132 | }, 133 | HelpToken = "helptoken" 134 | } 135 | } 136 | }); 137 | 138 | var runInfo = builder.Build(new string[] { "command", "1", "2", "3", "4", "5" }); 139 | 140 | var testRunInfo = runInfo as TestRunInfo; 141 | Assert.NotNull(testRunInfo); 142 | 143 | Assert.Equal(10, testRunInfo.Int1); 144 | Assert.Equal(20, testRunInfo.Int2); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/PropertyArgument/SuccessTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.PropertyArgument 8 | { 9 | public class PropertyArgumentSuccessTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class InSingleCommand 17 | { 18 | [Fact] 19 | public void ParsedArgumentValues_SuccessfullyBindToRunInfo() 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Commands.Add(new Command 24 | { 25 | Key = "command", 26 | Arguments = 27 | { 28 | new PropertyArgument 29 | { 30 | Property = ri => ri.Bool1 31 | }, 32 | new PropertyArgument 33 | { 34 | Property = ri => ri.Int1 35 | }, 36 | new PropertyArgument 37 | { 38 | Property = ri => ri.String1 39 | } 40 | } 41 | }); 42 | 43 | var runInfo = (TestRunInfo)builder.Build(new string[] { "command", "true", "99", "parsed" }); 44 | 45 | Assert.True(runInfo.Bool1); 46 | Assert.Equal(99, runInfo.Int1); 47 | Assert.Equal("parsed", runInfo.String1); 48 | } 49 | } 50 | 51 | public class InNestedCommand 52 | { 53 | [Fact] 54 | public void ParsedArgumentValues_SuccessfullyBindToRunInfo() 55 | { 56 | RunInfoBuilder builder = GetBuilder(); 57 | 58 | builder.Commands.Add(new Command 59 | { 60 | Key = "command", 61 | SubCommands = 62 | { 63 | new SubCommand 64 | { 65 | Key = "subcommand", 66 | Arguments = 67 | { 68 | new PropertyArgument 69 | { 70 | Property = ri => ri.Bool1 71 | }, 72 | new PropertyArgument 73 | { 74 | Property = ri => ri.Int1 75 | }, 76 | new PropertyArgument 77 | { 78 | Property = ri => ri.String1 79 | } 80 | } 81 | } 82 | } 83 | }); 84 | 85 | var runInfo = (TestRunInfo)builder.Build(new string[] { "command", "subcommand", "true", "99", "parsed" }); 86 | 87 | Assert.True(runInfo.Bool1); 88 | Assert.Equal(99, runInfo.Int1); 89 | Assert.Equal("parsed", runInfo.String1); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/SequenceArgument/FailTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.SequenceArgument 8 | { 9 | public class SequenceArgumentFailTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class InSingleCommand 17 | { 18 | [Fact] 19 | public void ExpectedArgument_ButNoMore_ProgramArguments_Throws() 20 | { 21 | Action testCode = () => 22 | { 23 | RunInfoBuilder builder = GetBuilder(); 24 | 25 | builder.Commands.Add(new Command 26 | { 27 | Key = "command", 28 | Arguments = 29 | { 30 | new SequenceArgument 31 | { 32 | ListProperty = ri => ri.StringList1 33 | } 34 | } 35 | }); 36 | 37 | builder.Build(new string[] { "command" }); 38 | }; 39 | 40 | Exception exception = Record.Exception(testCode); 41 | 42 | var processException = exception as ProcessException; 43 | 44 | Assert.NotNull(processException); 45 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 46 | Assert.Equal(0, processException.CommandLevel); 47 | } 48 | 49 | [Theory] 50 | [InlineData("a")] 51 | [InlineData("a", "1")] 52 | [InlineData("1", "a")] 53 | [InlineData("1", "2", "a")] 54 | [InlineData("1", "a", "2")] 55 | public void ProgramArguments_ContainsUnparseableValues_Throws(params string[] sequenceValues) 56 | { 57 | Action testCode = () => 58 | { 59 | RunInfoBuilder builder = GetBuilder(); 60 | 61 | builder.Commands.Add(new Command 62 | { 63 | Key = "command", 64 | Arguments = 65 | { 66 | new SequenceArgument 67 | { 68 | ListProperty = ri => ri.IntList1, 69 | OnParseErrorUseMessage = value => value + " from OnParseErrorUseMessage" 70 | } 71 | } 72 | }); 73 | 74 | var programArgs = new List { "command" }; 75 | programArgs.AddRange(sequenceValues); 76 | 77 | builder.Build(programArgs.ToArray()); 78 | }; 79 | 80 | Exception exception = Record.Exception(testCode); 81 | 82 | var processException = exception as ProcessException; 83 | 84 | Assert.NotNull(processException); 85 | Assert.Equal(ProcessError.ParserInvalidValue, processException.ErrorType); 86 | Assert.Equal(0, processException.CommandLevel); 87 | Assert.Equal("a from OnParseErrorUseMessage", processException.Message); 88 | } 89 | } 90 | 91 | public class InNestedSubCommand 92 | { 93 | [Fact] 94 | public void ExpectedArgument_ButNoMore_ProgramArguments_Throws() 95 | { 96 | Action testCode = () => 97 | { 98 | RunInfoBuilder builder = GetBuilder(); 99 | 100 | builder.Commands.Add(new Command 101 | { 102 | Key = "command", 103 | SubCommands = 104 | { 105 | new SubCommand 106 | { 107 | Key = "subcommand", 108 | Arguments = 109 | { 110 | new SequenceArgument 111 | { 112 | ListProperty = ri => ri.StringList1 113 | } 114 | } 115 | } 116 | } 117 | }); 118 | 119 | builder.Build(new string[] { "command", "subcommand" }); 120 | }; 121 | 122 | Exception exception = Record.Exception(testCode); 123 | 124 | var processException = exception as ProcessException; 125 | 126 | Assert.NotNull(processException); 127 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 128 | Assert.Equal(1, processException.CommandLevel); 129 | } 130 | 131 | [Theory] 132 | [InlineData("a")] 133 | [InlineData("a", "1")] 134 | [InlineData("1", "a")] 135 | [InlineData("1", "2", "a")] 136 | [InlineData("1", "a", "2")] 137 | public void ProgramArguments_ContainsUnparseableValues_Throws(params string[] sequenceValues) 138 | { 139 | Action testCode = () => 140 | { 141 | RunInfoBuilder builder = GetBuilder(); 142 | 143 | builder.Commands.Add(new Command 144 | { 145 | Key = "command", 146 | SubCommands = 147 | { 148 | new SubCommand 149 | { 150 | Key = "subcommand", 151 | Arguments = 152 | { 153 | new SequenceArgument 154 | { 155 | ListProperty = ri => ri.IntList1, 156 | OnParseErrorUseMessage = value => value + " from OnParseErrorUseMessage" 157 | } 158 | } 159 | } 160 | } 161 | }); 162 | 163 | var programArgs = new List { "command", "subcommand" }; 164 | programArgs.AddRange(sequenceValues); 165 | 166 | builder.Build(programArgs.ToArray()); 167 | }; 168 | 169 | Exception exception = Record.Exception(testCode); 170 | 171 | var processException = exception as ProcessException; 172 | 173 | Assert.NotNull(processException); 174 | Assert.Equal(ProcessError.ParserInvalidValue, processException.ErrorType); 175 | Assert.Equal(1, processException.CommandLevel); 176 | Assert.Equal("a from OnParseErrorUseMessage", processException.Message); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/SequenceArgument/SuccessTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.SequenceArgument 8 | { 9 | public class SequenceArgumentSuccessTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class InSingleCommand 17 | { 18 | [Fact] 19 | public void ParsedValues_SuccessfullyBind_ToListProperty() 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Commands.Add(new Command 24 | { 25 | Key = "command", 26 | Arguments = 27 | { 28 | new SequenceArgument 29 | { 30 | ListProperty = ri => ri.IntList1 31 | } 32 | } 33 | }); 34 | 35 | var runInfo = (TestRunInfo)builder.Build(new string[] { "command", "1", "2", "3" }); 36 | 37 | Assert.Equal(3, runInfo.IntList1.Count); 38 | Assert.Equal(1, runInfo.IntList1[0]); 39 | Assert.Equal(2, runInfo.IntList1[1]); 40 | Assert.Equal(3, runInfo.IntList1[2]); 41 | } 42 | } 43 | 44 | public class InNestedCommand 45 | { 46 | [Fact] 47 | public void ParsedValues_SuccessfullyBind_ToListProperty() 48 | { 49 | RunInfoBuilder builder = GetBuilder(); 50 | 51 | builder.Commands.Add(new Command 52 | { 53 | Key = "command", 54 | Arguments = 55 | { 56 | new SequenceArgument 57 | { 58 | ListProperty = ri => ri.IntList1 59 | } 60 | }, 61 | SubCommands = 62 | { 63 | new SubCommand 64 | { 65 | Key = "subcommand", 66 | Arguments = 67 | { 68 | new SequenceArgument 69 | { 70 | ListProperty = ri => ri.IntList2 71 | } 72 | } 73 | } 74 | } 75 | }); 76 | 77 | var runInfo = (TestRunInfo)builder.Build(new string[] 78 | { 79 | "command", "1", "2", "3", "subcommand", "4", "5", "6" 80 | }); 81 | 82 | Assert.Equal(3, runInfo.IntList1.Count); 83 | Assert.Equal(1, runInfo.IntList1[0]); 84 | Assert.Equal(2, runInfo.IntList1[1]); 85 | Assert.Equal(3, runInfo.IntList1[2]); 86 | 87 | Assert.Equal(3, runInfo.IntList2.Count); 88 | Assert.Equal(4, runInfo.IntList2[0]); 89 | Assert.Equal(5, runInfo.IntList2[1]); 90 | Assert.Equal(6, runInfo.IntList2[2]); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/SetArgument/FailTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.SetArgument 8 | { 9 | public class FailTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class InSingleCommand 17 | { 18 | [Fact] 19 | public void ExpectedArgument_ButNoMore_ProgramArguments_Throws() 20 | { 21 | Action testCode = () => 22 | { 23 | RunInfoBuilder builder = GetBuilder(); 24 | 25 | builder.Commands.Add(new Command 26 | { 27 | Key = "command", 28 | Arguments = 29 | { 30 | new SetArgument 31 | { 32 | Property = ri => ri.Bool1, 33 | Values = new List<(string, bool)> 34 | { 35 | ("true", true), ("false", false) 36 | } 37 | } 38 | } 39 | }); 40 | 41 | builder.Build(new string[] { "command" }); 42 | }; 43 | 44 | Exception exception = Record.Exception(testCode); 45 | 46 | var processException = exception as ProcessException; 47 | 48 | Assert.NotNull(processException); 49 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 50 | Assert.Equal(0, processException.CommandLevel); 51 | } 52 | 53 | [Fact] 54 | public void ProgramArgument_InvalidMatch_Throws() 55 | { 56 | Action testCode = () => 57 | { 58 | RunInfoBuilder builder = GetBuilder(); 59 | 60 | builder.Commands.Add(new Command 61 | { 62 | Key = "command", 63 | Arguments = 64 | { 65 | new SetArgument 66 | { 67 | Property = ri => ri.Bool1, 68 | Values = new List<(string, bool)> 69 | { 70 | ("true", true), ("false", false) 71 | } 72 | } 73 | } 74 | }); 75 | 76 | builder.Build(new string[] { "command", "invalid" }); 77 | }; 78 | 79 | Exception exception = Record.Exception(testCode); 80 | 81 | var processException = exception as ProcessException; 82 | 83 | Assert.NotNull(processException); 84 | Assert.Equal(ProcessError.UnknownValue, processException.ErrorType); 85 | Assert.Equal(0, processException.CommandLevel); 86 | } 87 | } 88 | 89 | public class InNestedCommand 90 | { 91 | [Fact] 92 | public void ExpectedArgument_ButNoMore_ProgramArguments_Throws() 93 | { 94 | Action testCode = () => 95 | { 96 | RunInfoBuilder builder = GetBuilder(); 97 | 98 | builder.Commands.Add(new Command 99 | { 100 | Key = "command", 101 | SubCommands = 102 | { 103 | new SubCommand 104 | { 105 | Key = "subcommand", 106 | Arguments = 107 | { 108 | new SetArgument 109 | { 110 | Property = ri => ri.Bool1, 111 | Values = new List<(string, bool)> 112 | { 113 | ("true", true), ("false", false) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | }); 120 | 121 | builder.Build(new string[] { "command", "subcommand" }); 122 | }; 123 | 124 | Exception exception = Record.Exception(testCode); 125 | 126 | var processException = exception as ProcessException; 127 | 128 | Assert.NotNull(processException); 129 | Assert.Equal(ProcessError.ExpectedProgramArgument, processException.ErrorType); 130 | Assert.Equal(1, processException.CommandLevel); 131 | } 132 | 133 | [Fact] 134 | public void ProgramArgument_InvalidMatch_Throws() 135 | { 136 | Action testCode = () => 137 | { 138 | RunInfoBuilder builder = GetBuilder(); 139 | 140 | builder.Commands.Add(new Command 141 | { 142 | Key = "command", 143 | SubCommands = 144 | { 145 | new SubCommand 146 | { 147 | Key = "subcommand", 148 | Arguments = 149 | { 150 | new SetArgument 151 | { 152 | Property = ri => ri.Bool1, 153 | Values = new List<(string, bool)> 154 | { 155 | ("true", true), ("false", false) 156 | } 157 | } 158 | } 159 | } 160 | } 161 | }); 162 | 163 | builder.Build(new string[] { "command", "subcommand", "invalid" }); 164 | }; 165 | 166 | Exception exception = Record.Exception(testCode); 167 | 168 | var processException = exception as ProcessException; 169 | 170 | Assert.NotNull(processException); 171 | Assert.Equal(ProcessError.UnknownValue, processException.ErrorType); 172 | Assert.Equal(1, processException.CommandLevel); 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Processing/SetArgument/SuccessTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Processing.SetArgument 8 | { 9 | public class SuccessTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class InSingleCommand 17 | { 18 | [Fact] 19 | public void Valid_ProgramArgumentValue_SuccessfullyBinds() 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Commands.Add(new Command 24 | { 25 | Key = "command", 26 | Arguments = 27 | { 28 | new SetArgument 29 | { 30 | Property = ri => ri.Bool1, 31 | Values = new List<(string, bool)> 32 | { 33 | ("true", true), ("false", false) 34 | } 35 | } 36 | } 37 | }); 38 | 39 | var runInfo = (TestRunInfo)builder.Build(new string[] { "command", "true" }); 40 | 41 | Assert.True(runInfo.Bool1); 42 | } 43 | } 44 | 45 | public class InNestedCommand 46 | { 47 | [Fact] 48 | public void Valid_ProgramArgumentValue_SuccessfullyBinds() 49 | { 50 | RunInfoBuilder builder = GetBuilder(); 51 | 52 | builder.Commands.Add(new Command 53 | { 54 | Key = "command", 55 | SubCommands = 56 | { 57 | new SubCommand 58 | { 59 | Key = "subcommand", 60 | Arguments = 61 | { 62 | new SetArgument 63 | { 64 | Property = ri => ri.Bool1, 65 | Values = new List<(string, bool)> 66 | { 67 | ("true", true), ("false", false) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | }); 74 | 75 | var runInfo = (TestRunInfo)builder.Build(new string[] { "command", "subcommand", "true" }); 76 | 77 | Assert.True(runInfo.Bool1); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Validations/CommandStoreTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Validations 8 | { 9 | public class CommandStoreTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | public class Command 17 | { 18 | [Fact] 19 | public void NullCommand_Throws() 20 | { 21 | Action testCode = () => 22 | { 23 | RunInfoBuilder builder = GetBuilder(); 24 | 25 | builder.Commands.Add(null); 26 | }; 27 | 28 | Exception exception = Record.Exception(testCode); 29 | 30 | var validationException = exception as CommandValidationException; 31 | 32 | Assert.NotNull(validationException); 33 | Assert.Equal(CommandValidationError.NullObject, validationException.ErrorType); 34 | Assert.Equal(0, validationException.CommandLevel); 35 | } 36 | 37 | [Theory] 38 | [InlineData(null)] 39 | [InlineData("")] 40 | public void InvalidKey_Throws(string key) 41 | { 42 | Action testCode = () => 43 | { 44 | RunInfoBuilder builder = GetBuilder(); 45 | 46 | builder.Commands.Add(new Command 47 | { 48 | Key = key 49 | }); 50 | }; 51 | 52 | Exception exception = Record.Exception(testCode); 53 | 54 | var validationException = exception as CommandValidationException; 55 | 56 | Assert.NotNull(validationException); 57 | Assert.Equal(CommandValidationError.KeyNotProvided, validationException.ErrorType); 58 | Assert.Equal(0, validationException.CommandLevel); 59 | } 60 | 61 | [Fact] 62 | public void DuplicateKey_Throws() 63 | { 64 | Action testCode = () => 65 | { 66 | RunInfoBuilder builder = GetBuilder(); 67 | 68 | builder.Commands.Add(new Command 69 | { 70 | Key = "command" 71 | }); 72 | 73 | builder.Commands.Add(new Command 74 | { 75 | Key = "command" 76 | }); 77 | }; 78 | 79 | Exception exception = Record.Exception(testCode); 80 | 81 | var validationException = exception as CommandValidationException; 82 | 83 | Assert.NotNull(validationException); 84 | Assert.Equal(CommandValidationError.DuplicateKey, validationException.ErrorType); 85 | Assert.Equal(0, validationException.CommandLevel); 86 | } 87 | } 88 | 89 | public class DefaultCommand 90 | { 91 | [Fact] 92 | public void NullCommand_Throws() 93 | { 94 | Action testCode = () => 95 | { 96 | RunInfoBuilder builder = GetBuilder(); 97 | 98 | builder.Commands.AddDefault(null); 99 | }; 100 | 101 | Exception exception = Record.Exception(testCode); 102 | 103 | var validationException = exception as CommandValidationException; 104 | 105 | Assert.NotNull(validationException); 106 | Assert.Equal(CommandValidationError.NullObject, validationException.ErrorType); 107 | Assert.Equal(-1, validationException.CommandLevel); 108 | } 109 | 110 | [Fact] 111 | public void DuplicateKey_Throws() 112 | { 113 | Action testCode = () => 114 | { 115 | RunInfoBuilder builder = GetBuilder(); 116 | 117 | builder.Commands.AddDefault(new DefaultCommand()); 118 | builder.Commands.AddDefault(new DefaultCommand()); 119 | }; 120 | 121 | Exception exception = Record.Exception(testCode); 122 | 123 | var validationException = exception as CommandValidationException; 124 | 125 | Assert.NotNull(validationException); 126 | Assert.Equal(CommandValidationError.DuplicateKey, validationException.ErrorType); 127 | Assert.Equal(-1, validationException.CommandLevel); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Test/FunctionalTests/Tests/Validations/OptionValidationTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.FunctionalTests.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace R5.RunInfoBuilder.FunctionalTests.Tests.Validations 8 | { 9 | public class OptionValidationTests 10 | { 11 | private static RunInfoBuilder GetBuilder() 12 | { 13 | return new RunInfoBuilder(); 14 | } 15 | 16 | [Fact] 17 | public void NullPropertyExpression_Throws() 18 | { 19 | Action testCode = () => 20 | { 21 | RunInfoBuilder builder = GetBuilder(); 22 | 23 | builder.Commands.Add(new Command 24 | { 25 | Key = "command", 26 | Options = 27 | { 28 | new Option 29 | { 30 | Key = "option", 31 | Property = null 32 | } 33 | } 34 | }); 35 | }; 36 | 37 | Exception exception = Record.Exception(testCode); 38 | 39 | var validationException = exception as CommandValidationException; 40 | 41 | Assert.NotNull(validationException); 42 | Assert.Equal(CommandValidationError.NullPropertyExpression, validationException.ErrorType); 43 | Assert.Equal(0, validationException.CommandLevel); 44 | } 45 | 46 | [Fact] 47 | public void Property_NotWritable_Throws() 48 | { 49 | Action testCode = () => 50 | { 51 | RunInfoBuilder builder = GetBuilder(); 52 | 53 | builder.Commands.Add(new Command 54 | { 55 | Key = "command", 56 | Options = 57 | { 58 | new Option 59 | { 60 | Key = "option", 61 | Property = ri => ri.UnwritableBool 62 | } 63 | } 64 | }); 65 | }; 66 | 67 | Exception exception = Record.Exception(testCode); 68 | 69 | var validationException = exception as CommandValidationException; 70 | 71 | Assert.NotNull(validationException); 72 | Assert.Equal(CommandValidationError.PropertyNotWritable, validationException.ErrorType); 73 | Assert.Equal(0, validationException.CommandLevel); 74 | } 75 | 76 | [Fact] 77 | public void OnProcess_NotAllowed_ForBoolOptions() 78 | { 79 | Action testCode = () => 80 | { 81 | RunInfoBuilder builder = GetBuilder(); 82 | 83 | builder.Commands.Add(new Command 84 | { 85 | Key = "command", 86 | Options = 87 | { 88 | new Option 89 | { 90 | Key = "option", 91 | Property = ri => ri.Bool1, 92 | OnParsed = arg => ProcessResult.Continue 93 | } 94 | } 95 | }); 96 | }; 97 | 98 | Exception exception = Record.Exception(testCode); 99 | 100 | var validationException = exception as CommandValidationException; 101 | 102 | Assert.NotNull(validationException); 103 | Assert.Equal(CommandValidationError.CallbackNotAllowed, validationException.ErrorType); 104 | Assert.Equal(0, validationException.CommandLevel); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Test/FunctionalTests/readme.md: -------------------------------------------------------------------------------- 1 | # Functional Tests (E2E) 2 | 3 | These tests are designed to cover specific scenarios. 4 | They should always begin with one of two starting points: 5 | 6 | 1. initializing RunInfoBuilder with its public constructor 7 | 2. using BuilderSetup for specialized RunInfoBuilder initialization to cover more advanced cases 8 | 9 | -------------------------------------------------------------------------------- /Test/UnitTests/Models/TestEnums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.UnitTests.Models 6 | { 7 | public enum TestEnum 8 | { 9 | ValueA, 10 | ValueB 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Test/UnitTests/Models/TestRunInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace R5.RunInfoBuilder.UnitTests.Models 6 | { 7 | public class TestRunInfo 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Test/UnitTests/Tests/Parser/EnumParsingTests.cs: -------------------------------------------------------------------------------- 1 | using R5.RunInfoBuilder.Parser; 2 | using R5.RunInfoBuilder.UnitTests.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using Xunit; 7 | 8 | namespace R5.RunInfoBuilder.UnitTests.Tests.Parser 9 | { 10 | public class EnumParsingTests 11 | { 12 | public class NullableEnum 13 | { 14 | public class TryParseAs 15 | { 16 | [Fact] 17 | public void EmptyValue_ReturnsFalse_WithNullParsedValue() 18 | { 19 | var parser = new ArgumentParser(); 20 | 21 | Type type = typeof(TestEnum?); 22 | string value = ""; 23 | 24 | bool result = parser.TryParseAs(type, value, out object parsed); 25 | 26 | Assert.True(result); 27 | Assert.Null(parsed); 28 | } 29 | 30 | [Fact] 31 | public void NonNullValue_ReturnsTrue_WithParsedValue() 32 | { 33 | var parser = new ArgumentParser(); 34 | 35 | Type type = typeof(TestEnum?); 36 | string value = "ValueB"; 37 | 38 | bool result = parser.TryParseAs(type, value, out object parsed); 39 | 40 | Assert.True(result); 41 | Assert.NotNull(parsed); 42 | Assert.Equal(TestEnum.ValueB, parsed); 43 | } 44 | } 45 | 46 | public class TryParseAsT 47 | { 48 | [Fact] 49 | public void EmptyValue_ReturnsFalse_WithNullParsedValue() 50 | { 51 | var parser = new ArgumentParser(); 52 | 53 | string value = ""; 54 | 55 | bool result = parser.TryParseAs(value, out TestEnum? parsed); 56 | 57 | Assert.True(result); 58 | Assert.Null(parsed); 59 | } 60 | 61 | [Fact] 62 | public void NonNullValue_ReturnsTrue_WithParsedValue() 63 | { 64 | var parser = new ArgumentParser(); 65 | 66 | string value = "ValueB"; 67 | 68 | bool result = parser.TryParseAs(value, out TestEnum? parsed); 69 | 70 | Assert.True(result); 71 | Assert.NotNull(parsed); 72 | Assert.Equal(TestEnum.ValueB, parsed); 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Test/UnitTests/Tests/Processor/OptionTokenizerTests.cs: -------------------------------------------------------------------------------- 1 | //using R5.RunInfoBuilder.Processor; 2 | //using R5.RunInfoBuilder.Processor.Models; 3 | //using System; 4 | //using System.Collections.Generic; 5 | //using System.Text; 6 | //using Xunit; 7 | 8 | //namespace R5.RunInfoBuilder.UnitTests.Tests.Processor 9 | //{ 10 | // public class OptionTokenizerTests 11 | // { 12 | // public class TokenizeKeyConfiguration_Method 13 | // { 14 | // [Theory] 15 | // [InlineData("full|s", "full", 's')] 16 | // [InlineData(" full|s ", "full", 's')] 17 | // [InlineData(" full | s ", "full", 's')] 18 | // [InlineData("full", "full", null)] 19 | // [InlineData(" full ", "full", null)] 20 | // public void TokenizeKeyConfiguration_ReturnsCorrectResults(string input, 21 | // string expectedFullKey, char? expectedShortKey) 22 | // { 23 | // (string fullKey, char? shortKey) = OptionTokenizer.TokenizeKeyConfiguration(input); 24 | 25 | // Assert.Equal(expectedFullKey, fullKey); 26 | // Assert.Equal(expectedShortKey, shortKey); 27 | // } 28 | // } 29 | 30 | // public class TokenizeProgramArgument_Method 31 | // { 32 | // [Fact] 33 | // public void DoesntStartWith_SingleOrDoubleDash_Throws() 34 | // { 35 | // Assert.Throws( 36 | // () => OptionTokenizer.TokenizeProgramArgument("invalid")); 37 | // } 38 | 39 | // [Fact] 40 | // public void MoreThanOneEquals_Throws() 41 | // { 42 | // Assert.Throws( 43 | // () => OptionTokenizer.TokenizeProgramArgument("--inv=ali=d")); 44 | // } 45 | 46 | // [Fact] 47 | // public void EndsWithEquals_Throws() 48 | // { 49 | // Assert.Throws( 50 | // () => OptionTokenizer.TokenizeProgramArgument("--invalid=")); 51 | // } 52 | 53 | // [Theory] 54 | // [InlineData("--=invalid")] 55 | // [InlineData("-=invalid")] 56 | // public void KeyBeginsWithEquals_Throws(string input) 57 | // { 58 | // Assert.Throws( 59 | // () => OptionTokenizer.TokenizeProgramArgument(input)); 60 | // } 61 | 62 | // [Theory] 63 | // [InlineData("-aa")] 64 | // [InlineData("-aba")] 65 | // [InlineData("-aab")] 66 | // [InlineData("-baa")] 67 | // public void Stacked_ContainsDuplicates_Throws(string input) 68 | // { 69 | // Assert.Throws( 70 | // () => OptionTokenizer.TokenizeProgramArgument(input)); 71 | // } 72 | 73 | // [Theory] 74 | // [InlineData("--full", OptionType.Full, "full", null)] 75 | // [InlineData("--full=value", OptionType.Full, "full", "value")] 76 | // [InlineData("-f", OptionType.Short, null, null, 'f')] 77 | // [InlineData("-f=value", OptionType.Short, null, "value", 'f')] 78 | // [InlineData("-stacked", OptionType.Stacked, null, null, 79 | // 's', 't', 'a', 'c', 'k', 'e', 'd')] 80 | // [InlineData("-stacked=value", OptionType.Stacked, null, "value", 81 | // 's', 't', 'a', 'c', 'k', 'e', 'd')] 82 | // internal void ValidInput_Returns_CorrectResult(string input, 83 | // OptionType expectedType, string expectedFullKey, 84 | // string expectedValue, params char[] expectedShortKeys) 85 | // { 86 | // (OptionType type, string fullKey, List shortKeys, string value) 87 | // = OptionTokenizer.TokenizeProgramArgument(input); 88 | 89 | // Assert.Equal(expectedType, type); 90 | // Assert.Equal(expectedFullKey, fullKey); 91 | // Assert.Equal(expectedValue, value); 92 | 93 | // if (expectedShortKeys.Length == 0) 94 | // { 95 | // Assert.Null(shortKeys); 96 | // } 97 | // else 98 | // { 99 | // Assert.True(listsEqualByElements(expectedShortKeys, shortKeys)); 100 | // } 101 | 102 | // bool listsEqualByElements(char[] a, List l) 103 | // { 104 | // if (a.Length != l.Count) 105 | // { 106 | // return false; 107 | // } 108 | 109 | // for (int i = 0; i < a.Length; i++) 110 | // { 111 | // if (a[i] != l[i]) 112 | // { 113 | // return false; 114 | // } 115 | // } 116 | 117 | // return true; 118 | // } 119 | // } 120 | // } 121 | // } 122 | //} 123 | -------------------------------------------------------------------------------- /Test/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | R5.RunInfoBuilder.UnitTests 6 | R5.RunInfoBuilder.UnitTests 7 | Debug;Release;Test 8 | 9 | 10 | 11 | latest 12 | 13 | 14 | 15 | latest 16 | 17 | 18 | 19 | latest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 2.0.0 2 | image: Visual Studio 2017 3 | configuration: Release 4 | 5 | before_build: 6 | - cmd: nuget restore 7 | 8 | build: 9 | project: RunInfoBuilder.sln 10 | verbosity: minimal 11 | 12 | artifacts: 13 | - path: '**\*.nupkg' --------------------------------------------------------------------------------