();
51 | }
52 | #endregion
53 |
54 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
55 | #if NETCOREAPP2_2
56 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
57 | #else
58 | public void Configure(IApplicationBuilder app, IHostEnvironment env)
59 | #endif
60 | {
61 | if (env.IsDevelopment())
62 | {
63 | app.UseDeveloperExceptionPage();
64 | }
65 | else
66 | {
67 | app.UseExceptionHandler("/Home/Error");
68 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
69 | app.UseHsts();
70 | }
71 |
72 | app.UseHttpsRedirection();
73 | app.UseStaticFiles();
74 | app.UseCookiePolicy();
75 |
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Home Page";
3 | }
4 |
5 |
9 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/Home/Privacy.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | ViewData["Title"] = "Privacy Policy";
3 | }
4 | @ViewData["Title"]
5 |
6 | Use this page to detail your site's privacy policy.
7 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/Shared/Error.cshtml:
--------------------------------------------------------------------------------
1 | @model ErrorViewModel
2 | @{
3 | ViewData["Title"] = "Error";
4 | }
5 |
6 | Error.
7 | An error occurred while processing your request.
8 |
9 | @if (Model.ShowRequestId)
10 | {
11 |
12 | Request ID: @Model.RequestId
13 |
14 | }
15 |
16 | Development Mode
17 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |
26 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/Shared/_CookieConsentPartial.cshtml:
--------------------------------------------------------------------------------
1 | @using Microsoft.AspNetCore.Http.Features
2 |
3 | @{
4 | var consentFeature = Context.Features.Get();
5 | var showBanner = !consentFeature?.CanTrack ?? false;
6 | var cookieString = consentFeature?.CreateConsentCookie();
7 | }
8 |
9 | @if (showBanner)
10 | {
11 |
12 | Use this space to summarize your privacy and cookie use policy.
Learn More .
13 |
14 | Accept
15 |
16 |
17 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/Shared/_ValidationScriptsPartial.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using MvcApp
2 | @using MvcApp.Models
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 |
--------------------------------------------------------------------------------
/src/MvcApp/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "_Layout";
3 | }
4 |
--------------------------------------------------------------------------------
/src/MvcApp/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/MvcApp/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/src/MvcApp/description.html:
--------------------------------------------------------------------------------
1 |
2 | The First Part The Second Part The Third Part The First Part Description of the first part
back to top... The Second Part Description of the second part
back to top... The Third Part The third part Html back to top...
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/css/site.css:
--------------------------------------------------------------------------------
1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
2 | for details on configuring this project to bundle and minify static web assets. */
3 |
4 | a.navbar-brand {
5 | white-space: normal;
6 | text-align: center;
7 | word-break: break-all;
8 | }
9 |
10 | /* Sticky footer styles
11 | -------------------------------------------------- */
12 | html {
13 | font-size: 14px;
14 | }
15 | @media (min-width: 768px) {
16 | html {
17 | font-size: 16px;
18 | }
19 | }
20 |
21 | .border-top {
22 | border-top: 1px solid #e5e5e5;
23 | }
24 | .border-bottom {
25 | border-bottom: 1px solid #e5e5e5;
26 | }
27 |
28 | .box-shadow {
29 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
30 | }
31 |
32 | button.accept-policy {
33 | font-size: 1rem;
34 | line-height: inherit;
35 | }
36 |
37 | /* Sticky footer styles
38 | -------------------------------------------------- */
39 | html {
40 | position: relative;
41 | min-height: 100%;
42 | }
43 |
44 | body {
45 | /* Margin bottom by footer height */
46 | margin-bottom: 60px;
47 | }
48 | .footer {
49 | position: absolute;
50 | bottom: 0;
51 | width: 100%;
52 | white-space: nowrap;
53 | /* Set the fixed height of the footer here */
54 | height: 60px;
55 | line-height: 60px; /* Vertically center the text there */
56 | }
57 |
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasperFx/oakton/3aec61cb8641d903cea76e7ba21176c2083249ae/src/MvcApp/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/js/site.js:
--------------------------------------------------------------------------------
1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
2 | // for details on configuring this project to bundle and minify static web assets.
3 |
4 | // Write your JavaScript code.
5 |
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/lib/bootstrap/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2018 Twitter, Inc.
4 | Copyright (c) 2011-2018 The Bootstrap Authors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) .NET Foundation. All rights reserved.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
4 | these files except in compliance with the License. You may obtain a copy of the
5 | License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software distributed
10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the
12 | specific language governing permissions and limitations under the License.
13 |
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/lib/jquery-validation/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright Jörn Zaefferer
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/MvcApp/wwwroot/lib/jquery/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright JS Foundation and other contributors, https://js.foundation/
2 |
3 | This software consists of voluntary contributions made by many
4 | individuals. For exact contribution history, see the revision history
5 | available at https://github.com/jquery/jquery
6 |
7 | The following license applies to all parts of this software except as
8 | documented below:
9 |
10 | ====
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining
13 | a copy of this software and associated documentation files (the
14 | "Software"), to deal in the Software without restriction, including
15 | without limitation the rights to use, copy, modify, merge, publish,
16 | distribute, sublicense, and/or sell copies of the Software, and to
17 | permit persons to whom the Software is furnished to do so, subject to
18 | the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
31 | ====
32 |
33 | All files located in the node_modules and external directories are
34 | externally maintained libraries used by this software which have their
35 | own licenses; we recommend you read them, as their terms may differ from
36 | the terms above.
37 |
--------------------------------------------------------------------------------
/src/Net5WebApi/Controllers/WeatherForecastController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.Extensions.Logging;
7 |
8 | namespace Net5WebApi.Controllers
9 | {
10 | [ApiController]
11 | [Route("[controller]")]
12 | public class WeatherForecastController : ControllerBase
13 | {
14 | private static readonly string[] Summaries = new[]
15 | {
16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
17 | };
18 |
19 | private readonly ILogger _logger;
20 |
21 | public WeatherForecastController(ILogger logger)
22 | {
23 | _logger = logger;
24 | }
25 |
26 | [HttpGet]
27 | public IEnumerable Get()
28 | {
29 | var rng = new Random();
30 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast
31 | {
32 | Date = DateTime.Now.AddDays(index),
33 | TemperatureC = rng.Next(-20, 55),
34 | Summary = Summaries[rng.Next(Summaries.Length)]
35 | })
36 | .ToArray();
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Net5WebApi/Net5WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net7.0;net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Net5WebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:18717",
8 | "sslPort": 44371
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "swagger",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "Net5WebApi": {
21 | "commandName": "Project",
22 | "commandLineArgs": "job1",
23 | "dotnetRunMessages": "true",
24 | "launchBrowser": true,
25 | "launchUrl": "swagger",
26 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
27 | "environmentVariables": {
28 | "ASPNETCORE_ENVIRONMENT": "Development"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Net5WebApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 | using Microsoft.OpenApi.Models;
14 |
15 | namespace Net5WebApi
16 | {
17 | public class Startup
18 | {
19 | public Startup(IConfiguration configuration)
20 | {
21 | Configuration = configuration;
22 | }
23 |
24 | public IConfiguration Configuration { get; }
25 |
26 | // This method gets called by the runtime. Use this method to add services to the container.
27 | public void ConfigureServices(IServiceCollection services)
28 | {
29 | services.AddControllers();
30 | services.AddSwaggerGen(c =>
31 | {
32 | c.SwaggerDoc("v1", new OpenApiInfo {Title = "Net5WebApi", Version = "v1"});
33 | });
34 | }
35 |
36 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
37 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
38 | {
39 | if (env.IsDevelopment())
40 | {
41 | app.UseDeveloperExceptionPage();
42 | app.UseSwagger();
43 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Net5WebApi v1"));
44 | }
45 |
46 | app.UseHttpsRedirection();
47 |
48 | app.UseRouting();
49 |
50 | app.UseAuthorization();
51 |
52 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Net5WebApi/WeatherForecast.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Net5WebApi
4 | {
5 | public class WeatherForecast
6 | {
7 | public DateTime Date { get; set; }
8 |
9 | public int TemperatureC { get; set; }
10 |
11 | public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);
12 |
13 | public string Summary { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Net5WebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Net5WebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/src/Oakton/ActivatorCommandCreator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | public class ActivatorCommandCreator : ICommandCreator
6 | {
7 | public IOaktonCommand CreateCommand(Type commandType)
8 | {
9 | return (IOaktonCommand)Activator.CreateInstance(commandType);
10 | }
11 |
12 | public object CreateModel(Type modelType)
13 | {
14 | return Activator.CreateInstance(modelType);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Oakton/Argument.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using JasperFx.Core;
5 | using JasperFx.Core.Reflection;
6 | using Oakton.Internal.Conversion;
7 | using Oakton.Parsing;
8 |
9 | namespace Oakton;
10 |
11 | public class Argument : TokenHandlerBase
12 | {
13 | private readonly MemberInfo _member;
14 | protected Func _converter;
15 | private bool _isLatched;
16 | private readonly Type _memberType;
17 |
18 | public Argument(MemberInfo member, Conversions conversions) : base(member)
19 | {
20 | _member = member;
21 | _memberType = member.GetMemberType();
22 | _converter = conversions.FindConverter(_memberType);
23 | }
24 |
25 | public override bool Handle(object input, Queue tokens)
26 | {
27 | if (_isLatched)
28 | {
29 | return false;
30 | }
31 |
32 | if (tokens.NextIsFlag())
33 | {
34 | if (_memberType.IsNumeric())
35 | {
36 | if (!decimal.TryParse(tokens.Peek(), out var number))
37 | {
38 | return false;
39 | }
40 | }
41 | else
42 | {
43 | return false;
44 | }
45 | }
46 |
47 | var value = _converter(tokens.Dequeue());
48 | setValue(input, value);
49 |
50 | _isLatched = true;
51 |
52 | return true;
53 | }
54 |
55 | public override string ToUsageDescription()
56 | {
57 | var memberType = _member.GetMemberType();
58 | if (memberType.GetTypeInfo().IsEnum)
59 | {
60 | return Enum.GetNames(memberType).Join("|");
61 | }
62 |
63 | return $"<{_member.Name.ToLower()}>";
64 | }
65 | }
--------------------------------------------------------------------------------
/src/Oakton/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("Tests")]
--------------------------------------------------------------------------------
/src/Oakton/CommandFailureException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | public class CommandFailureException : Exception
6 | {
7 | public CommandFailureException(string message) : base(message)
8 | {
9 | }
10 | }
--------------------------------------------------------------------------------
/src/Oakton/CommandRun.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace Oakton;
4 |
5 | public class CommandRun
6 | {
7 | public IOaktonCommand Command { get; set; }
8 | public object Input { get; set; }
9 |
10 | public Task Execute()
11 | {
12 | return Command.Execute(Input);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Oakton/Commands/CheckEnvironmentCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using JasperFx.Core;
5 | using Oakton.Environment;
6 | using Spectre.Console;
7 |
8 | namespace Oakton.Commands;
9 |
10 | public class CheckEnvironmentInput : NetCoreInput
11 | {
12 | [Description("Use to optionally write the results of the environment checks to a file")]
13 | public string FileFlag { get; set; }
14 | }
15 |
16 | [Description("Execute all environment checks against the application", Name = "check-env")]
17 | public class CheckEnvironmentCommand : OaktonAsyncCommand
18 | {
19 | public override async Task Execute(CheckEnvironmentInput input)
20 | {
21 | AnsiConsole.Write(
22 | new FigletText("Oakton"){Justification = Justify.Left});
23 |
24 |
25 | using var host = input.BuildHost();
26 | var results = await EnvironmentChecker.ExecuteAllEnvironmentChecks(host.Services);
27 |
28 | if (input.FileFlag.IsNotEmpty())
29 | {
30 | results.WriteToFile(input.FileFlag);
31 | Console.WriteLine("Writing environment checks to " + input.FileFlag);
32 | }
33 |
34 | if (results.Failures.Any())
35 | {
36 | AnsiConsole.MarkupLine("[red]Some environment checks failed![/]");
37 | return false;
38 | }
39 |
40 | AnsiConsole.MarkupLine("[green]All environment checks are good![/]");
41 | return true;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Oakton/Commands/RunCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Loader;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Hosting;
8 |
9 | using Oakton.Environment;
10 |
11 | namespace Oakton.Commands;
12 |
13 | public class RunInput : NetCoreInput
14 | {
15 | [Description("Run the environment checks before starting the host")]
16 | public bool CheckFlag { get; set; }
17 | }
18 |
19 | [Description("Start and run this .Net application")]
20 | public class RunCommand : OaktonAsyncCommand
21 | {
22 | public override async Task Execute(RunInput input)
23 | {
24 | using var host = input.BuildHost();
25 | if (input.CheckFlag)
26 | {
27 | var checks = await EnvironmentChecker.ExecuteAllEnvironmentChecks(host.Services);
28 | checks.Assert();
29 | }
30 |
31 | var reset = new ManualResetEventSlim();
32 | // ReSharper disable once PossibleNullReferenceException
33 | AssemblyLoadContext.GetLoadContext(typeof(RunCommand).Assembly).Unloading +=
34 | (Action)(context => reset.Set());
35 | Console.CancelKeyPress += (ConsoleCancelEventHandler)((sender, eventArgs) =>
36 | {
37 | reset.Set();
38 | eventArgs.Cancel = true;
39 | });
40 |
41 | var lifetime = host.Services.GetService();
42 | lifetime?.ApplicationStopping.Register(() => reset.Set());
43 |
44 | await host.StartAsync();
45 | reset.Wait();
46 | await host.StopAsync();
47 | return true;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Oakton/DependencyInjectionCommandCreator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using System;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using JasperFx.Core;
6 | using JasperFx.Core.Reflection;
7 | using Oakton.Help;
8 |
9 | namespace Oakton;
10 |
11 | internal class DependencyInjectionCommandCreator : ICommandCreator
12 | {
13 | private readonly IServiceProvider _serviceProvider;
14 | public DependencyInjectionCommandCreator(IServiceProvider serviceProvider)
15 | {
16 | _serviceProvider = serviceProvider;
17 | }
18 |
19 | public IOaktonCommand CreateCommand(Type commandType)
20 | {
21 | if (commandType.GetProperties().Any(x => x.HasAttribute()))
22 | {
23 | return new WrappedOaktonCommand(_serviceProvider, commandType);
24 | }
25 |
26 | return ActivatorUtilities.CreateInstance(_serviceProvider, commandType) as IOaktonCommand;
27 | }
28 |
29 | public object CreateModel(Type modelType)
30 | {
31 | return Activator.CreateInstance(modelType)!;
32 | }
33 | }
34 |
35 | internal class WrappedOaktonCommand : IOaktonCommand
36 | {
37 | private readonly AsyncServiceScope _scope;
38 | private readonly IOaktonCommand _inner;
39 |
40 | public WrappedOaktonCommand(IServiceProvider provider, Type commandType)
41 | {
42 | _scope = provider.CreateAsyncScope();
43 | _inner = (IOaktonCommand)_scope.ServiceProvider.GetRequiredService(commandType);
44 | }
45 |
46 | public Type InputType => _inner.InputType;
47 | public UsageGraph Usages => _inner.Usages;
48 | public async Task Execute(object input)
49 | {
50 | await using (_scope)
51 | {
52 | // Execute your actual command
53 | return await _inner.Execute(input);
54 | }
55 | }
56 | }
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/Oakton/DescriptionAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Adds a textual description to arguments or flags on input classes, or on a command class
7 | ///
8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Property)]
9 | public class DescriptionAttribute : Attribute
10 | {
11 | public DescriptionAttribute(string description)
12 | {
13 | Description = description;
14 | }
15 |
16 | public string Description { get; set; }
17 |
18 | public string Name { get; set; }
19 | }
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/DescribeInput.cs:
--------------------------------------------------------------------------------
1 | namespace Oakton.Descriptions;
2 |
3 | public class DescribeInput : NetCoreInput
4 | {
5 | [Description("Optionally write the description to the given file location")]
6 | public string FileFlag { get; set; } = null;
7 |
8 | [Description("Do not write any output to the console")]
9 | public bool SilentFlag { get; set; } = false;
10 |
11 | [Description("Filter the output to only a single described part")]
12 | public string TitleFlag { get; set; }
13 |
14 | [Description("If set, the command only lists the known part titles")]
15 | public bool ListFlag { get; set; }
16 |
17 | [Description("If set, interactively select which part(s) to preview")]
18 | public bool InteractiveFlag { get; set; }
19 | }
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/IDescribedSystemPart.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 |
4 | namespace Oakton.Descriptions;
5 |
6 | #region sample_IDescribedSystemPart
7 |
8 | ///
9 | /// Base class for a "described" part of your application.
10 | /// Implementations of this type should be registered in your
11 | /// system's DI container to be exposed through the "describe"
12 | /// command
13 | ///
14 | public interface IDescribedSystemPart
15 | {
16 | ///
17 | /// A descriptive title to be shown in the rendered output
18 | ///
19 | string Title { get; }
20 |
21 | ///
22 | /// Write markdown formatted text to describe this system part
23 | ///
24 | ///
25 | ///
26 | Task Write(TextWriter writer);
27 | }
28 |
29 | #endregion
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/IDescribedSystemPartFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Oakton.Descriptions;
2 |
3 | #region sample_IDescribedSystemPartFactory
4 |
5 | ///
6 | /// Register implementations of this service to help
7 | /// the describe command discover additional system parts
8 | ///
9 | public interface IDescribedSystemPartFactory
10 | {
11 | IDescribedSystemPart[] Parts();
12 | }
13 |
14 | #endregion
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/IDescribesProperties.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Oakton.Descriptions;
4 |
5 | ///
6 | /// Interface to expose key/value pairs to diagnostic output
7 | ///
8 | public interface IDescribesProperties
9 | {
10 | IDictionary DescribeProperties();
11 | }
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/IRequiresServices.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton.Descriptions;
4 |
5 | internal interface IRequiresServices
6 | {
7 | void Resolve(IServiceProvider services);
8 | }
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/ITreeDescriber.cs:
--------------------------------------------------------------------------------
1 | using Spectre.Console;
2 |
3 | namespace Oakton.Descriptions;
4 |
5 | ///
6 | /// Interface to expose additional diagnostic information to a Spectre tree node
7 | ///
8 | public interface ITreeDescriber
9 | {
10 | void Describe(TreeNode parentNode);
11 | }
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/IWriteToConsole.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace Oakton.Descriptions;
4 |
5 | ///
6 | /// Optional interface for exposing specialized console output
7 | /// in the "describe" command
8 | ///
9 | public interface IWriteToConsole
10 | {
11 | Task WriteToConsole();
12 | }
--------------------------------------------------------------------------------
/src/Oakton/Descriptions/LambdaDescribedSystemPart.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace Oakton.Descriptions;
7 |
8 | internal class LambdaDescribedSystemPart : IDescribedSystemPart, IRequiresServices
9 | {
10 | private readonly Func _write;
11 | private T _service;
12 |
13 | public LambdaDescribedSystemPart(string title, Func write)
14 | {
15 | Title = title;
16 | _write = write;
17 | }
18 |
19 | public string Title { get; }
20 |
21 | public Task Write(TextWriter writer)
22 | {
23 | return _service == null ? default : _write(_service, writer);
24 | }
25 |
26 | public void Resolve(IServiceProvider services)
27 | {
28 | _service = services.GetService();
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Oakton/Environment/EnvironmentCheckException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 |
4 | namespace Oakton.Environment;
5 |
6 | public class EnvironmentCheckException : AggregateException
7 | {
8 | public EnvironmentCheckException(EnvironmentCheckResults results) : base(results.ToString(),
9 | results.Failures.Select(x => x.Exception))
10 | {
11 | Results = results;
12 | }
13 |
14 | public EnvironmentCheckResults Results { get; }
15 | }
--------------------------------------------------------------------------------
/src/Oakton/Environment/EnvironmentChecker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Oakton.Resources;
8 | using Spectre.Console;
9 |
10 | namespace Oakton.Environment;
11 |
12 | ///
13 | /// Executes the environment checks registered in an IoC container
14 | ///
15 | public static class EnvironmentChecker
16 | {
17 | public static async Task ExecuteAllEnvironmentChecks(IServiceProvider services,
18 | CancellationToken token = default)
19 | {
20 | var results = new EnvironmentCheckResults();
21 |
22 | var checks = services.discoverChecks().ToArray();
23 | if (!checks.Any())
24 | {
25 | AnsiConsole.WriteLine("No environment checks.");
26 | return results;
27 | }
28 |
29 | await AnsiConsole.Progress().StartAsync(async c =>
30 | {
31 | var task = c.AddTask("[bold]Running Environment Checks[/]", new ProgressTaskSettings
32 | {
33 | MaxValue = checks.Length
34 | });
35 |
36 | for (var i = 0; i < checks.Length; i++)
37 | {
38 | var check = checks[i];
39 |
40 | try
41 | {
42 | await check.Assert(services, token);
43 |
44 | AnsiConsole.MarkupLine(
45 | $"[green]{(i + 1).ToString().PadLeft(4)}.) Success: {check.Description}[/]");
46 |
47 | results.RegisterSuccess(check.Description);
48 | }
49 | catch (Exception e)
50 | {
51 | AnsiConsole.MarkupLine(
52 | $"[red]{(i + 1).ToString().PadLeft(4)}.) Failed: {check.Description}[/]");
53 | AnsiConsole.WriteException(e);
54 |
55 | results.RegisterFailure(check.Description, e);
56 | }
57 | finally
58 | {
59 | task.Increment(1);
60 | }
61 | }
62 |
63 | task.StopTask();
64 | });
65 |
66 | return results;
67 | }
68 |
69 | private static IEnumerable discoverChecks(this IServiceProvider services)
70 | {
71 | foreach (var check in services.GetServices()) yield return check;
72 |
73 | foreach (var factory in services.GetServices())
74 | {
75 | foreach (var check in factory.Build()) yield return check;
76 | }
77 |
78 | foreach (var resource in services.GetServices())
79 | yield return new ResourceEnvironmentCheck(resource);
80 |
81 | foreach (var source in services.GetServices())
82 | {
83 | foreach (var resource in source.FindResources()) yield return new ResourceEnvironmentCheck(resource);
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/Oakton/Environment/FileExistsCheck.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace Oakton.Environment;
7 |
8 | public class FileExistsCheck : IEnvironmentCheck
9 | {
10 | private readonly string _file;
11 |
12 | public FileExistsCheck(string file)
13 | {
14 | _file = file;
15 | }
16 |
17 |
18 | public Task Assert(IServiceProvider services, CancellationToken cancellation)
19 | {
20 | if (!File.Exists(_file))
21 | {
22 | throw new Exception($"File '{_file}' cannot be found!");
23 | }
24 |
25 | return Task.CompletedTask;
26 | }
27 |
28 | public string Description => ToString();
29 |
30 | public override string ToString()
31 | {
32 | return $"File '{_file}' exists";
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Oakton/Environment/IEnvironmentCheck.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Oakton.Environment;
6 |
7 | #region sample_IEnvironmentCheck
8 |
9 | ///
10 | /// Executed during bootstrapping time to carry out environment tests
11 | /// against the application
12 | ///
13 | public interface IEnvironmentCheck
14 | {
15 | ///
16 | /// A textual description for command line output that describes
17 | /// what is being checked
18 | ///
19 | string Description { get; }
20 |
21 | ///
22 | /// Asserts that the current check is valid. Throw an exception
23 | /// to denote a failure
24 | ///
25 | Task Assert(IServiceProvider services, CancellationToken cancellation);
26 | }
27 |
28 | #endregion
--------------------------------------------------------------------------------
/src/Oakton/Environment/IEnvironmentCheckFactory.cs:
--------------------------------------------------------------------------------
1 | namespace Oakton.Environment;
2 |
3 | public interface IEnvironmentCheckFactory
4 | {
5 | IEnvironmentCheck[] Build();
6 | }
--------------------------------------------------------------------------------
/src/Oakton/Environment/LambdaCheck.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Oakton.Environment;
6 |
7 | public class LambdaCheck : IEnvironmentCheck
8 | {
9 | private readonly Func _action;
10 |
11 | public LambdaCheck(string description, Func action)
12 | {
13 | Description = description;
14 | _action = action;
15 | }
16 |
17 | public Task Assert(IServiceProvider services, CancellationToken cancellation)
18 | {
19 | return _action(services, cancellation);
20 | }
21 |
22 | public string Description { get; }
23 |
24 | public override string ToString()
25 | {
26 | return Description;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Oakton/FlagAliasAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Use to override the long and/or short flag keys of a property or field
7 | ///
8 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
9 | public class FlagAliasAttribute : Attribute
10 | {
11 | public FlagAliasAttribute(string longAlias, char oneLetterAlias)
12 | {
13 | LongAlias = longAlias;
14 | OneLetterAlias = oneLetterAlias;
15 | }
16 |
17 | public FlagAliasAttribute(char oneLetterAlias)
18 | {
19 | OneLetterAlias = oneLetterAlias;
20 | }
21 |
22 | public FlagAliasAttribute(string longAlias)
23 | {
24 | LongAlias = longAlias;
25 | }
26 |
27 | public FlagAliasAttribute(string longAlias, bool longAliasOnly)
28 | {
29 | LongAlias = longAlias;
30 | LongAliasOnly = longAliasOnly;
31 | }
32 |
33 | public string LongAlias { get; }
34 |
35 |
36 | public char? OneLetterAlias { get; }
37 |
38 | public bool LongAliasOnly { get; }
39 | }
--------------------------------------------------------------------------------
/src/Oakton/Help/CommandUsage.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using JasperFx.Core;
4 | using Oakton.Parsing;
5 | using Spectre.Console;
6 |
7 | namespace Oakton.Help;
8 |
9 | public class CommandUsage
10 | {
11 | public string Description { get; set; }
12 | public IEnumerable Arguments { get; set; }
13 | public IEnumerable ValidFlags { get; set; }
14 |
15 | public string ToUsage(string appName, string commandName)
16 | {
17 | var arguments = Arguments.Union(ValidFlags)
18 | .Select(x => x.ToUsageDescription())
19 | .Join(" ");
20 |
21 | return $"{appName} {commandName} {arguments}";
22 | }
23 |
24 | public void WriteUsage(string appName, string commandName)
25 | {
26 | var arguments = Arguments.Union(ValidFlags)
27 | .Select(x => x.ToUsageDescription())
28 | .Join(" ");
29 |
30 | AnsiConsole.MarkupLine($"[bold]{appName}[/] [bold]{commandName}[/] [cyan][{arguments}][/]");
31 | }
32 |
33 |
34 | public bool IsValidUsage(IEnumerable handlers)
35 | {
36 | var actualArgs = handlers.OfType();
37 | if (actualArgs.Count() != Arguments.Count())
38 | {
39 | return false;
40 | }
41 |
42 | if (!Arguments.All(x => actualArgs.Contains(x)))
43 | {
44 | return false;
45 | }
46 |
47 | var flags = handlers.Where(x => !(x is Argument));
48 | return flags.All(x => ValidFlags.Contains(x));
49 | }
50 | }
--------------------------------------------------------------------------------
/src/Oakton/Help/HelpCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Spectre.Console;
4 |
5 | namespace Oakton.Help;
6 |
7 | [Description("List all the available commands", Name = "help")]
8 | public class HelpCommand : OaktonCommand
9 | {
10 | public HelpCommand()
11 | {
12 | Usage("List all the available commands").Arguments(x => x.Name);
13 | Usage("Show all the valid usages for a command");
14 | }
15 |
16 | public override bool Execute(HelpInput input)
17 | {
18 | if (input.Usage != null)
19 | {
20 | input.Usage.WriteUsages(input.AppName);
21 | return false;
22 | }
23 |
24 | if (input.InvalidCommandName)
25 | {
26 | writeInvalidCommand(input.Name);
27 | listAllCommands(input);
28 | return false;
29 | }
30 |
31 | listAllCommands(input);
32 | return true;
33 | }
34 |
35 | private void listAllCommands(HelpInput input)
36 | {
37 | if (!input.CommandTypes.Any())
38 | {
39 | Console.WriteLine("There are no known commands in this executable!");
40 | return;
41 | }
42 |
43 | AnsiConsole.WriteLine();
44 | AnsiConsole.MarkupLine("[bold]The available commands are:[/]");
45 |
46 | var table = new Table
47 | {
48 | Border = TableBorder.SimpleHeavy
49 | };
50 |
51 | table.AddColumns("Alias", "Description");
52 | foreach (var type in input.CommandTypes.OrderBy(CommandFactory.CommandNameFor))
53 | table.AddRow(CommandFactory.CommandNameFor(type), CommandFactory.DescriptionFor(type));
54 |
55 | AnsiConsole.Write(table);
56 | AnsiConsole.WriteLine();
57 | AnsiConsole.MarkupLine(
58 | "Use [italic]dotnet run -- ? [[command name]][/] or [italic]dotnet run -- help [[command name]][/] to see usage help about a specific command");
59 | }
60 |
61 | private void writeInvalidCommand(string commandName)
62 | {
63 | AnsiConsole.WriteLine();
64 | AnsiConsole.MarkupLine($"[red]'{commandName}' is not a command. See available commands.[/]");
65 | AnsiConsole.WriteLine();
66 | AnsiConsole.WriteLine();
67 | }
68 | }
--------------------------------------------------------------------------------
/src/Oakton/Help/HelpInput.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Oakton.Help;
5 |
6 | public class HelpInput
7 | {
8 | [IgnoreOnCommandLine] public IEnumerable CommandTypes { get; set; }
9 |
10 | [Description("A command name")] public string Name { get; set; }
11 |
12 | [IgnoreOnCommandLine] public bool InvalidCommandName { get; set; }
13 |
14 | [IgnoreOnCommandLine] public UsageGraph Usage { get; set; }
15 |
16 | [IgnoreOnCommandLine] public string AppName { get; set; } = "dotnet run --";
17 | }
--------------------------------------------------------------------------------
/src/Oakton/HostWrapperCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using Oakton.Help;
7 |
8 | namespace Oakton;
9 |
10 | internal class HostWrapperCommand : IOaktonCommand
11 | {
12 | private readonly IOaktonCommand _inner;
13 | private readonly Func _hostSource;
14 | private readonly PropertyInfo[] _props;
15 |
16 | public HostWrapperCommand(IOaktonCommand inner, Func hostSource, PropertyInfo[] props)
17 | {
18 | _inner = inner;
19 | _hostSource = hostSource;
20 | _props = props;
21 | }
22 |
23 | public Type InputType => _inner.InputType;
24 | public UsageGraph Usages => _inner.Usages;
25 | public async Task Execute(object input)
26 | {
27 | var host = _hostSource();
28 | try
29 | {
30 | await using var scope = host.Services.CreateAsyncScope();
31 | foreach (var prop in _props)
32 | {
33 | var serviceType = prop.PropertyType;
34 | var service = scope.ServiceProvider.GetRequiredService(serviceType);
35 | prop.SetValue(_inner, service);
36 | }
37 |
38 | return await _inner.Execute(input);
39 | }
40 | finally
41 | {
42 | if (host is IAsyncDisposable ad)
43 | {
44 | await ad.DisposeAsync();
45 | }
46 | else
47 | {
48 | host.Dispose();
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/src/Oakton/ICommandCreator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Service locator for command types. The default just uses Activator.CreateInstance().
7 | /// Can be used to plug in IoC construction in Oakton applications
8 | ///
9 | public interface ICommandCreator
10 | {
11 | IOaktonCommand CreateCommand(Type commandType);
12 | object CreateModel(Type modelType);
13 | }
--------------------------------------------------------------------------------
/src/Oakton/ICommandFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Reflection;
3 | using Microsoft.Extensions.Hosting;
4 |
5 | namespace Oakton;
6 |
7 | ///
8 | /// Interface that Oakton uses to build command runs during execution. Can be used for custom
9 | /// command activation
10 | ///
11 | public interface ICommandFactory
12 | {
13 | CommandRun BuildRun(string commandLine);
14 | CommandRun BuildRun(IEnumerable args);
15 | void RegisterCommands(Assembly assembly);
16 |
17 | IEnumerable BuildAllCommands();
18 |
19 | void ApplyExtensions(IHostBuilder builder);
20 | }
--------------------------------------------------------------------------------
/src/Oakton/IHostBuilderInput.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Hosting;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Interface used to get access to the HostBuilder from command inputs.
7 | ///
8 | public interface IHostBuilderInput
9 | {
10 | [IgnoreOnCommandLine] IHostBuilder HostBuilder { get; set; }
11 | }
--------------------------------------------------------------------------------
/src/Oakton/IOaktonCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Oakton.Help;
4 |
5 | namespace Oakton;
6 |
7 | public interface IOaktonCommand
8 | {
9 | Type InputType { get; }
10 | UsageGraph Usages { get; }
11 | Task Execute(object input);
12 | }
13 |
14 | public interface IOaktonCommand : IOaktonCommand
15 | {
16 | }
--------------------------------------------------------------------------------
/src/Oakton/IServiceRegistrations.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Implementations of this interface can be used to define
7 | /// service registrations to be loaded by Oakton command extensions
8 | ///
9 | public interface IServiceRegistrations
10 | {
11 | void Configure(IServiceCollection services);
12 | }
--------------------------------------------------------------------------------
/src/Oakton/IgnoreOnCommandLineAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Oakton ignores any fields or properties with this attribute during the binding to the input
7 | /// objects
8 | ///
9 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
10 | public class IgnoreOnCommandLineAttribute : Attribute
11 | {
12 | }
--------------------------------------------------------------------------------
/src/Oakton/InjectServiceAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | ///
6 | /// Decorate Oakton commands that are being called by
7 | ///
8 | [AttributeUsage(AttributeTargets.Property)]
9 | public class InjectServiceAttribute : Attribute
10 | {
11 |
12 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/ArgsExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace Oakton.Internal;
4 |
5 | public static class ArgsExtensions
6 | {
7 | public static string[] FilterLauncherArgs(this string[] args)
8 | {
9 | if (args == null)
10 | {
11 | return new string[0];
12 | }
13 |
14 | while (args.Any() && args[0].StartsWith("%") && args[0].EndsWith("%"))
15 | {
16 | args = args.Skip(1).ToArray();
17 | }
18 |
19 | return args;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/ArrayConversion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using JasperFx.Core;
3 |
4 | namespace Oakton.Internal.Conversion;
5 |
6 | public class ArrayConversion : IConversionProvider
7 | {
8 | private readonly Conversions _conversions;
9 |
10 | public ArrayConversion(Conversions conversions)
11 | {
12 | _conversions = conversions;
13 | }
14 |
15 | public Func? ConverterFor(Type type)
16 | {
17 | if (!type.IsArray)
18 | {
19 | return null;
20 | }
21 |
22 | var innerType = type.GetElementType()!;
23 | var inner = _conversions.FindConverter(innerType);
24 |
25 | return stringValue =>
26 | {
27 | if (stringValue.ToUpper() == "EMPTY" || stringValue.Trim().IsEmpty())
28 | {
29 | return Array.CreateInstance(innerType, 0);
30 | }
31 |
32 | var strings = stringValue.ToDelimitedArray();
33 | var array = Array.CreateInstance(innerType, strings.Length);
34 |
35 | for (var i = 0; i < strings.Length; i++)
36 | {
37 | var value = inner?.Invoke(strings[i]);
38 | array.SetValue(value, i);
39 | }
40 |
41 | return array;
42 | };
43 | }
44 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/Conversions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using JasperFx.Core;
5 |
6 | namespace Oakton.Internal.Conversion;
7 |
8 | public class Conversions
9 | {
10 | private readonly LightweightCache> _convertors;
11 | private readonly IList _providers = new List();
12 |
13 |
14 | public Conversions()
15 | {
16 | _convertors =
17 | new LightweightCache?>(
18 | type => { return providers().Select(x => x.ConverterFor(type)).FirstOrDefault(x => x != null); });
19 |
20 | RegisterConversion(bool.Parse);
21 | RegisterConversion(byte.Parse);
22 | RegisterConversion(sbyte.Parse);
23 | RegisterConversion(char.Parse);
24 | RegisterConversion(decimal.Parse);
25 | RegisterConversion(double.Parse);
26 | RegisterConversion(float.Parse);
27 | RegisterConversion(short.Parse);
28 | RegisterConversion(int.Parse);
29 | RegisterConversion(long.Parse);
30 | RegisterConversion(ushort.Parse);
31 | RegisterConversion(uint.Parse);
32 | RegisterConversion(ulong.Parse);
33 | RegisterConversion(DateTimeConverter.GetDateTime);
34 | RegisterConversion(Guid.Parse);
35 |
36 | RegisterConversion(x =>
37 | {
38 | if (x == "EMPTY")
39 | {
40 | return string.Empty;
41 | }
42 |
43 | return x;
44 | });
45 | }
46 |
47 |
48 | private IEnumerable providers()
49 | {
50 | foreach (var provider in _providers) yield return provider;
51 |
52 | yield return new EnumerationConversion();
53 | yield return new NullableConvertor(this);
54 | yield return new ArrayConversion(this);
55 | yield return new StringConverterProvider();
56 | }
57 |
58 | public void RegisterConversionProvider() where T : IConversionProvider, new()
59 | {
60 | _providers.Add(new T());
61 | }
62 |
63 | public void RegisterConversion(Func convertor)
64 | {
65 | _convertors[typeof(T)] = x => convertor(x);
66 | }
67 |
68 | public Func? FindConverter(Type type)
69 | {
70 | return _convertors[type];
71 | }
72 |
73 | public object? Convert(Type type, string raw)
74 | {
75 | return _convertors[type]?.Invoke(raw);
76 | }
77 |
78 | public bool Has(Type type)
79 | {
80 | return _convertors.Contains(type) || providers().Any(x => x.ConverterFor(type) != null);
81 | }
82 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/DateTimeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Linq;
4 | using System.Text.RegularExpressions;
5 |
6 | namespace Oakton.Internal.Conversion;
7 |
8 | public class DateTimeConverter
9 | {
10 | public const string TODAY = "TODAY";
11 |
12 | private static readonly Regex iso8601Expression = new(
13 | @"^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$");
14 |
15 | public static DateTime GetDateTime(string dateString)
16 | {
17 | var trimmedString = dateString.Trim();
18 | if (trimmedString == TODAY)
19 | {
20 | return DateTime.Today;
21 | }
22 |
23 | if (trimmedString.Contains(TODAY))
24 | {
25 | var dayString = trimmedString.Substring(5, trimmedString.Length - 5);
26 | var days = int.Parse(dayString);
27 |
28 | return DateTime.Today.AddDays(days);
29 | }
30 |
31 | if (isDayOfWeek(dateString))
32 | {
33 | return convertToDateFromDayAndTime(dateString);
34 | }
35 |
36 | if (iso8601Expression.IsMatch(trimmedString))
37 | {
38 | //Thank you jon skeet : http://stackoverflow.com/questions/10029099/datetime-parse2012-09-30t230000-0000000z-always-converts-to-datetimekind-l
39 | DateTime result;
40 | var success = DateTime.TryParseExact(trimmedString, "yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'",
41 | CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
42 | out result);
43 |
44 | if (success)
45 | {
46 | return result;
47 | }
48 | }
49 |
50 | return DateTime.Parse(trimmedString);
51 | }
52 |
53 | private static DateTime convertToDateFromDayAndTime(string dateString)
54 | {
55 | dateString = dateString.Replace(" ", " ");
56 | var parts = dateString.Split(' ');
57 | var day = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), parts[0], true);
58 | var minutes = minutesFrom24HourTime(parts[1]);
59 |
60 | var date = DateTime.Today.AddMinutes(minutes);
61 | while (date.DayOfWeek != day)
62 | {
63 | date = date.AddDays(1);
64 | }
65 |
66 | return date;
67 | }
68 |
69 | private static bool isDayOfWeek(string text)
70 | {
71 | var days = Enum.GetNames(typeof(DayOfWeek));
72 | return days.FirstOrDefault(x => text.ToLower().StartsWith(x.ToLower())) != null;
73 | }
74 |
75 |
76 | private static int minutesFrom24HourTime(string time)
77 | {
78 | var parts = time.Split(':');
79 | return 60 * int.Parse(parts[0]) + int.Parse(parts[1]);
80 | }
81 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/EnumerationConversion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 |
4 | namespace Oakton.Internal.Conversion;
5 |
6 | // SAMPLE: EnumerationConversion
7 | public class EnumerationConversion : IConversionProvider
8 | {
9 | public Func? ConverterFor(Type type)
10 | {
11 | if (type.GetTypeInfo().IsEnum)
12 | {
13 | return x => Enum.Parse(type, x);
14 | }
15 |
16 | return null;
17 | }
18 | }
19 | // ENDSAMPLE
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/IConversionProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton.Internal.Conversion;
4 |
5 | // SAMPLE: IConversionProvider
6 | public interface IConversionProvider
7 | {
8 | // Given the type argument, either return a
9 | // Func that can parse a string into that Type
10 | // or return null to let another IConversionProvider
11 | // handle this type
12 | Func? ConverterFor(Type type);
13 | }
14 | // ENDSAMPLE
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/NullableConvertor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using JasperFx.Core.Reflection;
4 |
5 | namespace Oakton.Internal.Conversion;
6 |
7 | public class NullableConvertor : IConversionProvider
8 | {
9 | private readonly Conversions _conversions;
10 |
11 | public NullableConvertor(Conversions conversions)
12 | {
13 | _conversions = conversions;
14 | }
15 |
16 | public Func? ConverterFor(Type type)
17 | {
18 | if (!type.IsNullable())
19 | {
20 | return null;
21 | }
22 |
23 |
24 | var innerType = type.GetGenericArguments().First();
25 | var inner = _conversions.FindConverter(innerType);
26 |
27 | return str => str == "NULL" ? null : inner?.Invoke(str);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/StringConverterProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using JasperFx.Core.Reflection;
4 |
5 | namespace Oakton.Internal.Conversion;
6 |
7 | public class StringConverterProvider : IConversionProvider
8 | {
9 | public Func? ConverterFor(Type type)
10 | {
11 | if (!type.IsConcrete())
12 | {
13 | return null;
14 | }
15 |
16 | var constructor = type.GetConstructor(new[] { typeof(string) });
17 | if (constructor == null)
18 | {
19 | return null;
20 | }
21 |
22 | var param = Expression.Parameter(typeof(string), "arg");
23 | var body = Expression.New(constructor, param);
24 |
25 | return Expression.Lambda>(body, param).Compile();
26 | }
27 | }
--------------------------------------------------------------------------------
/src/Oakton/Internal/Conversion/TimeSpanConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace Oakton.Internal.Conversion;
6 |
7 | public class TimeSpanConverter
8 | {
9 | private const string TimespanPattern =
10 | @"
11 | ^(?\d+ # quantity is expressed as some digits
12 | (\.\d+)?) # optionally followed by a decimal point or colon and more digits
13 | \s* # optional whitespace
14 | (?[a-z]*) # units is expressed as a word
15 | $ # match the entire string";
16 |
17 |
18 | public static TimeSpan GetTimeSpan(string timeString)
19 | {
20 | var match = Regex.Match(timeString.Trim(), TimespanPattern, RegexOptions.IgnorePatternWhitespace);
21 | if (!match.Success)
22 | {
23 | return TimeSpan.Parse(timeString);
24 | }
25 |
26 |
27 | var number = double.Parse(match.Groups["quantity"].Value);
28 | var units = match.Groups["units"].Value.ToLower();
29 | switch (units)
30 | {
31 | case "s":
32 | case "second":
33 | case "seconds":
34 | return TimeSpan.FromSeconds(number);
35 |
36 | case "m":
37 | case "minute":
38 | case "minutes":
39 | return TimeSpan.FromMinutes(number);
40 |
41 | case "h":
42 | case "hour":
43 | case "hours":
44 | return TimeSpan.FromHours(number);
45 |
46 | case "d":
47 | case "day":
48 | case "days":
49 | return TimeSpan.FromDays(number);
50 | }
51 |
52 | if (timeString.Length == 4 && !timeString.Contains(":"))
53 | {
54 | var hours = int.Parse(timeString.Substring(0, 2));
55 | var minutes = int.Parse(timeString.Substring(2, 2));
56 |
57 | return new TimeSpan(hours, minutes, 0);
58 | }
59 |
60 | if (timeString.Length == 5 && timeString.Contains(":"))
61 | {
62 | var parts = timeString.Split(':');
63 | var hours = int.Parse(parts.ElementAt(0));
64 | var minutes = int.Parse(parts.ElementAt(1));
65 |
66 | return new TimeSpan(hours, minutes, 0);
67 | }
68 |
69 | throw new Exception("Time periods must be expressed in seconds, minutes, hours, or days.");
70 | }
71 | }
--------------------------------------------------------------------------------
/src/Oakton/InvalidUsageException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | public class InvalidUsageException : Exception
6 | {
7 | public InvalidUsageException() : base(string.Empty)
8 | {
9 | }
10 |
11 | public InvalidUsageException(string message) : base(message)
12 | {
13 | }
14 |
15 | public InvalidUsageException(string message, Exception innerException) : base(message, innerException)
16 | {
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Oakton/Oakton.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Command Line Parsing and Execution
4 | Oakton
5 | 6.3.0
6 | Jeremy D. Miller
7 | net6.0;net7.0;net8.0;net9.0
8 | portable
9 | Oakton
10 | 11.0
11 | Library
12 | Oakton
13 | Command Line Parsing
14 | https://jasperfx.github.io/oakton
15 | MIT
16 | git
17 | git://github.com/jasperfx/oakton
18 | https://raw.githubusercontent.com/JasperFx/JasperFx.Core/main/jasperfx-logo.jpg?raw=true
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Oakton/OaktonAsyncCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Oakton.Help;
4 |
5 | namespace Oakton;
6 |
7 | ///
8 | /// Base class for all Oakton commands
9 | ///
10 | ///
11 | public abstract class OaktonAsyncCommand : IOaktonCommand
12 | {
13 | protected OaktonAsyncCommand()
14 | {
15 | Usages = new UsageGraph(GetType());
16 | }
17 |
18 | public UsageGraph Usages { get; }
19 |
20 | public Type InputType => typeof(T);
21 |
22 | Task IOaktonCommand.Execute(object input)
23 | {
24 | return Execute((T)input);
25 | }
26 |
27 | ///
28 | /// If your command has multiple argument usage patterns ala the Git command line, use
29 | /// this method to define the valid combinations of arguments and optionally limit the flags that are valid
30 | /// for each usage
31 | ///
32 | /// The description of this usage to be displayed from the CLI help command
33 | ///
34 | public UsageGraph.UsageExpression Usage(string description)
35 | {
36 | return Usages.AddUsage(description);
37 | }
38 |
39 | ///
40 | /// The actual execution of the command. Return "false" to denote failures
41 | /// or "true" for successes
42 | ///
43 | ///
44 | ///
45 | public abstract Task Execute(T input);
46 | }
--------------------------------------------------------------------------------
/src/Oakton/OaktonCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Oakton.Help;
4 |
5 | namespace Oakton;
6 |
7 | ///
8 | /// Base class for all Oakton commands
9 | ///
10 | ///
11 | public abstract class OaktonCommand : IOaktonCommand
12 | {
13 | protected OaktonCommand()
14 | {
15 | Usages = new UsageGraph(GetType());
16 | }
17 |
18 | public UsageGraph Usages { get; }
19 |
20 | public Type InputType => typeof(T);
21 |
22 | Task IOaktonCommand.Execute(object input)
23 | {
24 | return Task.FromResult(Execute((T)input));
25 | }
26 |
27 | ///
28 | /// If your command has multiple argument usage patterns ala the Git command line, use
29 | /// this method to define the valid combinations of arguments and optionally limit the flags that are valid
30 | /// for each usage
31 | ///
32 | /// The description of this usage to be displayed from the CLI help command
33 | ///
34 | public UsageGraph.UsageExpression Usage(string description)
35 | {
36 | return Usages.AddUsage(description);
37 | }
38 |
39 | ///
40 | /// The actual execution of the command. Return "false" to denote failures
41 | /// or "true" for successes
42 | ///
43 | ///
44 | ///
45 | public abstract bool Execute(T input);
46 | }
--------------------------------------------------------------------------------
/src/Oakton/OaktonCommandAssemblyAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using JasperFx.Core.Reflection;
3 |
4 | namespace Oakton;
5 |
6 | ///
7 | /// If the CommandExecutor is configured to discover assemblies,
8 | /// this attribute on an assembly will cause Oakton to search for
9 | /// command types within this assembly
10 | ///
11 | [AttributeUsage(AttributeTargets.Assembly)]
12 | public class OaktonCommandAssemblyAttribute : Attribute
13 | {
14 | public OaktonCommandAssemblyAttribute()
15 | {
16 | }
17 |
18 | ///
19 | /// Concrete type implementing the IServiceRegistrations interface that should
20 | /// automatically be applied to hosts during environment checks or resource
21 | /// commands
22 | ///
23 | ///
24 | public OaktonCommandAssemblyAttribute(Type extensionType)
25 | {
26 | if (extensionType.HasDefaultConstructor() && extensionType.CanBeCastTo())
27 | {
28 | ExtensionType = extensionType;
29 | }
30 | else
31 | {
32 | throw new ArgumentOutOfRangeException(nameof(extensionType),
33 | $"Extension types must have a default, no arg constructor and implement the {nameof(IServiceRegistrations)} interface");
34 | }
35 | }
36 |
37 | public Type ExtensionType { get; set; }
38 | }
--------------------------------------------------------------------------------
/src/Oakton/OaktonEnvironment.cs:
--------------------------------------------------------------------------------
1 | namespace Oakton;
2 |
3 | public static class OaktonEnvironment
4 | {
5 | ///
6 | /// If using Oakton as the run command in .Net Core applications with WebApplication,
7 | /// this will force Oakton to automatically start up the IHost when the Program.Main()
8 | /// method runs. Very useful for WebApplicationFactory testing
9 | ///
10 | public static bool AutoStartHost { get; set; }
11 | }
--------------------------------------------------------------------------------
/src/Oakton/OaktonOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton;
4 |
5 | public class OaktonOptions
6 | {
7 | public string OptionsFile { get; set; }
8 | public Action Factory { get; set; }
9 | public string DefaultCommand { get; set; } = "run";
10 | }
11 |
--------------------------------------------------------------------------------
/src/Oakton/Parsing/ArgPreprocessor.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Oakton.Parsing;
4 |
5 | public class ArgPreprocessor
6 | {
7 | public static IEnumerable Process(IEnumerable incomingArgs)
8 | {
9 | var newArgs = new List();
10 |
11 | foreach (var arg in incomingArgs)
12 | {
13 | if (isMultiArg(arg))
14 | {
15 | foreach (var c in arg.TrimStart('-')) newArgs.Add("-" + c);
16 | }
17 | else
18 | {
19 | newArgs.Add(arg);
20 | }
21 | }
22 |
23 | return newArgs;
24 | }
25 |
26 | private static bool isMultiArg(string arg)
27 | {
28 | // Getting around GH-24
29 | if (decimal.TryParse(arg, out var number))
30 | {
31 | return false;
32 | }
33 |
34 | // regular short args look like '-a', multi-args are '-abc' which is really '-a -b -c'
35 | return InputParser.IsShortFlag(arg) && arg.Length > 2;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/BooleanFlag.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Reflection;
3 |
4 | namespace Oakton.Parsing;
5 |
6 | public class BooleanFlag : TokenHandlerBase
7 | {
8 | private readonly MemberInfo _member;
9 |
10 | public BooleanFlag(MemberInfo member) : base(member)
11 | {
12 | _member = member;
13 | }
14 |
15 | public override bool Handle(object input, Queue tokens)
16 | {
17 | if (!tokens.NextIsFlagFor(_member))
18 | {
19 | return false;
20 | }
21 |
22 | tokens.Dequeue();
23 | setValue(input, true);
24 |
25 | return true;
26 | }
27 |
28 | public override string ToUsageDescription()
29 | {
30 | return $"[{InputParser.ToFlagAliases(_member)}]";
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/DictionaryFlag.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using JasperFx.Core.Reflection;
6 |
7 | namespace Oakton.Parsing;
8 |
9 | public class DictionaryFlag : TokenHandlerBase
10 | {
11 | private readonly string _prefix;
12 |
13 | public DictionaryFlag(MemberInfo member) : base(member)
14 | {
15 | if (!member.GetMemberType().CanBeCastTo>())
16 | {
17 | throw new ArgumentOutOfRangeException("Dictionary flag types have to be IDictionary");
18 | }
19 |
20 | var flagAliases = InputParser.ToFlagAliases(Member);
21 |
22 | _prefix = flagAliases.LongForm + ":";
23 | }
24 |
25 |
26 | public override bool Handle(object input, Queue tokens)
27 | {
28 | if (tokens.Peek().StartsWith(_prefix))
29 | {
30 | var flag = tokens.Dequeue();
31 |
32 | if (tokens.Count == 0)
33 | {
34 | throw new InvalidUsageException($"No value specified for flag {flag}.");
35 | }
36 |
37 | var key = flag.Split(':').Last().Trim();
38 | var rawValue = tokens.Dequeue();
39 |
40 | var dict = getValue(input) as IDictionary;
41 | if (dict == null)
42 | {
43 | dict = new Dictionary();
44 | setValue(input, dict);
45 | }
46 |
47 | if (dict.ContainsKey(key))
48 | {
49 | dict[key] = rawValue;
50 | }
51 | else
52 | {
53 | dict.Add(key, rawValue);
54 | }
55 |
56 | return true;
57 | }
58 |
59 | return false;
60 | }
61 |
62 | public override string ToUsageDescription()
63 | {
64 | return $"[{_prefix} ]";
65 | }
66 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/EnumerableArgument.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using JasperFx.Core.Reflection;
6 | using Oakton.Internal.Conversion;
7 |
8 | namespace Oakton.Parsing;
9 |
10 | public class EnumerableArgument : Argument
11 | {
12 | private readonly MemberInfo _member;
13 |
14 | public EnumerableArgument(MemberInfo member, Conversions conversions) : base(member, conversions)
15 | {
16 | _member = member;
17 |
18 | _converter = conversions.FindConverter(member.GetMemberType().DetermineElementType());
19 | }
20 |
21 | public override bool Handle(object input, Queue tokens)
22 | {
23 | var elementType = _member.GetMemberType().GetGenericArguments().First();
24 | var list = typeof(List<>).CloseAndBuildAs(elementType);
25 |
26 | var wasHandled = false;
27 | while (tokens.Count > 0 && !tokens.NextIsFlag())
28 | {
29 | var value = _converter(tokens.Dequeue());
30 | list.Add(value);
31 |
32 | wasHandled = true;
33 | }
34 |
35 | if (wasHandled)
36 | {
37 | setValue(input, list);
38 | }
39 |
40 | return wasHandled;
41 | }
42 |
43 | public override string ToUsageDescription()
44 | {
45 | var name = _member.Name.ToLower();
46 | return $"<{name}1 {name}2 {name}3 ...>";
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/EnumerableFlag.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using JasperFx.Core.Reflection;
5 | using Oakton.Internal.Conversion;
6 |
7 | namespace Oakton.Parsing;
8 |
9 | public class EnumerableFlag : Flag
10 | {
11 | private readonly MemberInfo _member;
12 |
13 | public EnumerableFlag(MemberInfo member, Conversions conversions)
14 | : base(member, member.GetMemberType().DetermineElementType(), conversions)
15 | {
16 | _member = member;
17 | }
18 |
19 | public override bool Handle(object input, Queue tokens)
20 | {
21 | var elementType = _member.GetMemberType().DetermineElementType();
22 | var list = typeof(List<>).CloseAndBuildAs(elementType);
23 |
24 | var wasHandled = false;
25 |
26 | if (tokens.NextIsFlagFor(_member))
27 | {
28 | var flag = tokens.Dequeue();
29 | while (tokens.Count > 0 && !tokens.NextIsFlag())
30 | {
31 | var value = Converter(tokens.Dequeue());
32 | list.Add(value);
33 |
34 | wasHandled = true;
35 | }
36 |
37 | if (!wasHandled)
38 | {
39 | throw new InvalidUsageException($"No values specified for flag {flag}.");
40 | }
41 |
42 | setValue(input, list);
43 | }
44 |
45 | return wasHandled;
46 | }
47 |
48 | public override string ToUsageDescription()
49 | {
50 | var flagAliases = InputParser.ToFlagAliases(_member);
51 |
52 | var name = InputParser.RemoveFlagSuffix(_member.Name).ToLower();
53 | return $"[{flagAliases} <{name}1 {name}2 {name}3 ...>]";
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/Flag.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using JasperFx.Core;
5 | using JasperFx.Core.Reflection;
6 | using Oakton.Internal.Conversion;
7 |
8 | namespace Oakton.Parsing;
9 |
10 | public class Flag : TokenHandlerBase
11 | {
12 | private readonly MemberInfo _member;
13 | protected Func Converter;
14 |
15 | public Flag(MemberInfo member, Conversions conversions) : this(member, member.GetMemberType(), conversions)
16 | {
17 | }
18 |
19 | public Flag(MemberInfo member, Type propertyType, Conversions conversions) : base(member)
20 | {
21 | _member = member;
22 | Converter = conversions.FindConverter(propertyType);
23 |
24 | if (Converter == null)
25 | {
26 | throw new ArgumentOutOfRangeException(
27 | $"Cannot derive a conversion for type {member.GetMemberType()} on property {member.Name}");
28 | }
29 | }
30 |
31 | public override bool Handle(object input, Queue tokens)
32 | {
33 | if (tokens.NextIsFlagFor(_member))
34 | {
35 | var flag = tokens.Dequeue();
36 |
37 | if (tokens.Count == 0)
38 | {
39 | throw new InvalidUsageException($"No value specified for flag {flag}.");
40 | }
41 |
42 | var rawValue = tokens.Dequeue();
43 | var value = Converter(rawValue);
44 |
45 | setValue(input, value);
46 |
47 | return true;
48 | }
49 |
50 |
51 | return false;
52 | }
53 |
54 | public override string ToUsageDescription()
55 | {
56 | var flagAliases = InputParser.ToFlagAliases(_member);
57 |
58 | if (_member.GetMemberType().GetTypeInfo().IsEnum)
59 | {
60 | var enumValues = Enum.GetNames(_member.GetMemberType()).Join("|");
61 | return $"[{flagAliases} {enumValues}]";
62 | }
63 |
64 | var name = InputParser.RemoveFlagSuffix(_member.Name).ToLower();
65 | return $"[{flagAliases} <{name}>]";
66 | }
67 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/FlagAliases.cs:
--------------------------------------------------------------------------------
1 | namespace Oakton.Parsing;
2 |
3 | public class FlagAliases
4 | {
5 | public string LongForm { get; set; }
6 | public string ShortForm { get; set; }
7 |
8 | public bool LongFormOnly { get; set; }
9 |
10 | public bool Matches(string token)
11 | {
12 | if (!LongFormOnly && InputParser.IsShortFlag(token))
13 | {
14 | return token == ShortForm;
15 | }
16 |
17 | var lowerToken = token.ToLower();
18 |
19 | return lowerToken == LongForm.ToLower();
20 | }
21 |
22 | public override string ToString()
23 | {
24 | return LongFormOnly ? $"{LongForm}" : $"{ShortForm}, {LongForm}";
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/ITokenHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Oakton.Parsing;
4 |
5 | public interface ITokenHandler
6 | {
7 | string Description { get; }
8 | string MemberName { get; }
9 | bool Handle(object input, Queue tokens);
10 |
11 | string ToUsageDescription();
12 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/OptionReader.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using JasperFx.Core;
4 |
5 | namespace Oakton.Parsing;
6 |
7 | public static class OptionReader
8 | {
9 | public static string Read(string file)
10 | {
11 | return File.ReadAllLines(file)
12 | .Select(x => x.Trim())
13 | .Where(x => x.IsNotEmpty())
14 | .Join(" ");
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/QueueExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Reflection;
3 |
4 | namespace Oakton.Parsing;
5 |
6 | public static class QueueExtensions
7 | {
8 | public static bool NextIsFlag(this Queue queue)
9 | {
10 | return InputParser.IsFlag(queue.Peek());
11 | }
12 |
13 | public static bool NextIsFlagFor(this Queue queue, MemberInfo property)
14 | {
15 | return InputParser.IsFlagFor(queue.Peek(), property);
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/StringTokenizer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Oakton.Parsing;
4 |
5 | public static class StringTokenizer
6 | {
7 | public static IEnumerable Tokenize(string content)
8 | {
9 | var searchString = content.Trim();
10 | if (searchString.Length == 0)
11 | {
12 | return new string[0];
13 | }
14 |
15 | var parser = new TokenParser();
16 |
17 | foreach (var c in content.ToCharArray()) parser.Read(c);
18 |
19 | // Gotta force the parser to know it's done
20 | parser.Read('\n');
21 |
22 | return parser.Tokens;
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/TokenHandlerBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Reflection;
3 | using JasperFx.Core.Reflection;
4 |
5 | namespace Oakton.Parsing;
6 |
7 | public abstract class TokenHandlerBase : ITokenHandler
8 | {
9 | protected TokenHandlerBase(MemberInfo member)
10 | {
11 | Member = member;
12 | }
13 |
14 | public MemberInfo Member { get; }
15 |
16 | public string Description
17 | {
18 | get
19 | {
20 | var name = Member.Name;
21 | Member.ForAttribute(att => name = att.Description);
22 |
23 | return name;
24 | }
25 | }
26 |
27 | public string MemberName => Member.Name;
28 |
29 | public abstract bool Handle(object input, Queue tokens);
30 | public abstract string ToUsageDescription();
31 |
32 | protected void setValue(object target, object value)
33 | {
34 | (Member as PropertyInfo)?.SetValue(target, value);
35 | (Member as FieldInfo)?.SetValue(target, value);
36 | }
37 |
38 | protected object getValue(object target)
39 | {
40 | return (Member as PropertyInfo)?.GetValue(target)
41 | ?? (Member as FieldInfo)?.GetValue(target);
42 | }
43 |
44 | public bool Equals(TokenHandlerBase other)
45 | {
46 | if (ReferenceEquals(null, other))
47 | {
48 | return false;
49 | }
50 |
51 | if (ReferenceEquals(this, other))
52 | {
53 | return true;
54 | }
55 |
56 | return other.Member.Equals(Member);
57 | }
58 |
59 | public override bool Equals(object obj)
60 | {
61 | if (ReferenceEquals(null, obj))
62 | {
63 | return false;
64 | }
65 |
66 | if (ReferenceEquals(this, obj))
67 | {
68 | return true;
69 | }
70 |
71 | if (obj.GetType() != typeof(TokenHandlerBase))
72 | {
73 | return false;
74 | }
75 |
76 | return Equals((TokenHandlerBase)obj);
77 | }
78 |
79 | public override int GetHashCode()
80 | {
81 | return Member != null ? Member.GetHashCode() : 0;
82 | }
83 | }
--------------------------------------------------------------------------------
/src/Oakton/Parsing/TokenParser.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Oakton.Parsing;
4 |
5 | public class TokenParser
6 | {
7 | private readonly List _tokens = new();
8 | private List _characters;
9 | private IMode _mode;
10 |
11 | public TokenParser()
12 | {
13 | _mode = new Searching(this);
14 | }
15 |
16 | public IEnumerable Tokens => _tokens;
17 |
18 | public void Read(char c)
19 | {
20 | _mode.Read(c);
21 | }
22 |
23 | private void addChar(char c)
24 | {
25 | _characters.Add(c);
26 | }
27 |
28 | private void startToken(IMode mode)
29 | {
30 | _mode = mode;
31 | _characters = new List();
32 | }
33 |
34 | private void endToken()
35 | {
36 | var @string = new string(_characters.ToArray());
37 | _tokens.Add(@string);
38 |
39 | _mode = new Searching(this);
40 | }
41 |
42 |
43 | public interface IMode
44 | {
45 | void Read(char c);
46 | }
47 |
48 | public class Searching : IMode
49 | {
50 | private readonly TokenParser _parent;
51 |
52 | public Searching(TokenParser parent)
53 | {
54 | _parent = parent;
55 | }
56 |
57 | public void Read(char c)
58 | {
59 | if (char.IsWhiteSpace(c))
60 | {
61 | return;
62 | }
63 |
64 | if (c == '"')
65 | {
66 | _parent.startToken(new InsideQuotedToken(_parent));
67 | }
68 | else
69 | {
70 | var normalToken = new InsideNormalToken(_parent);
71 | _parent.startToken(normalToken);
72 | normalToken.Read(c);
73 | }
74 | }
75 | }
76 |
77 | public class InsideQuotedToken : IMode
78 | {
79 | private readonly TokenParser _parent;
80 |
81 | public InsideQuotedToken(TokenParser parent)
82 | {
83 | _parent = parent;
84 | }
85 |
86 |
87 | public void Read(char c)
88 | {
89 | if (c == '"')
90 | {
91 | _parent.endToken();
92 | }
93 | else
94 | {
95 | _parent.addChar(c);
96 | }
97 | }
98 | }
99 |
100 | public class InsideNormalToken : IMode
101 | {
102 | private readonly TokenParser _parent;
103 |
104 | public InsideNormalToken(TokenParser parent)
105 | {
106 | _parent = parent;
107 | }
108 |
109 | public void Read(char c)
110 | {
111 | if (char.IsWhiteSpace(c))
112 | {
113 | _parent.endToken();
114 | }
115 | else
116 | {
117 | _parent.addChar(c);
118 | }
119 | }
120 | }
121 | }
--------------------------------------------------------------------------------
/src/Oakton/PreBuiltHostBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using Spectre.Console;
7 |
8 | namespace Oakton;
9 |
10 | internal class PreBuiltHostBuilder : IHostBuilder
11 | {
12 | private readonly string _notSupportedMessage;
13 |
14 | public PreBuiltHostBuilder(IHost host)
15 | {
16 | Host = host;
17 | _notSupportedMessage =
18 | $"The IHost ({Host}) is already constructed. See https://jasperfx.github.io/oakton for alternative bootstrapping to enable this feature.";
19 | }
20 |
21 | public IHost Host { get; }
22 |
23 | public IHostBuilder ConfigureHostConfiguration(Action configureDelegate)
24 | {
25 | AnsiConsole.MarkupLine("[yellow]Cannot override host configuration when the IHost is already constructed[/]");
26 | return this;
27 | }
28 |
29 | public IHostBuilder ConfigureAppConfiguration(Action configureDelegate)
30 | {
31 | AnsiConsole.MarkupLine("[yellow]Cannot override app configuration when the IHost is already constructed[/]");
32 | return this;
33 | }
34 |
35 | public IHostBuilder ConfigureServices(Action configureDelegate)
36 | {
37 | AnsiConsole.MarkupLine("[yellow]Cannot override services when the IHost is already constructed[/]");
38 | return this;
39 | }
40 |
41 | public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory)
42 | {
43 | throw new NotSupportedException(_notSupportedMessage);
44 | }
45 |
46 | public IHostBuilder UseServiceProviderFactory(
47 | Func> factory)
48 | {
49 | throw new NotSupportedException(_notSupportedMessage);
50 | }
51 |
52 | public IHostBuilder ConfigureContainer(
53 | Action configureDelegate)
54 | {
55 | throw new NotSupportedException(_notSupportedMessage);
56 | }
57 |
58 | public IHost Build()
59 | {
60 | return Host;
61 | }
62 |
63 | public IDictionary Properties { get; } = new Dictionary();
64 | }
--------------------------------------------------------------------------------
/src/Oakton/Resources/IStatefulResource.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Spectre.Console.Rendering;
5 |
6 | namespace Oakton.Resources;
7 |
8 | #region sample_IStatefulResourceWithDependencies
9 |
10 | ///
11 | /// Use to create dependencies between
12 | ///
13 | public interface IStatefulResourceWithDependencies : IStatefulResource
14 | {
15 | // Given all the known stateful resources in your system -- including the current resource!
16 | // tell Oakton which resources are dependencies of this resource that should be setup first
17 | IEnumerable FindDependencies(IReadOnlyList others);
18 | }
19 |
20 | #endregion
21 |
22 | #region sample_IStatefulResource
23 |
24 | ///
25 | /// Adapter interface used by Oakton enabled applications to allow
26 | /// Oakton to setup/teardown/clear the state/check on stateful external
27 | /// resources of the system like databases or messaging queues
28 | ///
29 | public interface IStatefulResource
30 | {
31 | ///
32 | /// Categorical type name of this resource for filtering
33 | ///
34 | string Type { get; }
35 |
36 | ///
37 | /// Identifier for this resource
38 | ///
39 | string Name { get; }
40 |
41 | ///
42 | /// Check whether the configuration for this resource is valid. An exception
43 | /// should be thrown if the check is invalid
44 | ///
45 | ///
46 | ///
47 | Task Check(CancellationToken token);
48 |
49 | ///
50 | /// Clear any persisted state within this resource
51 | ///
52 | ///
53 | ///
54 | Task ClearState(CancellationToken token);
55 |
56 | ///
57 | /// Tear down the stateful resource represented by this implementation
58 | ///
59 | ///
60 | ///
61 | Task Teardown(CancellationToken token);
62 |
63 | ///
64 | /// Make any necessary configuration to this stateful resource
65 | /// to make the system function correctly
66 | ///
67 | ///
68 | ///
69 | Task Setup(CancellationToken token);
70 |
71 | ///
72 | /// Optionally return a report of the current state of this resource
73 | ///
74 | ///
75 | ///
76 | Task DetermineStatus(CancellationToken token);
77 | }
78 |
79 | #endregion
--------------------------------------------------------------------------------
/src/Oakton/Resources/IStatefulResourceSource.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Oakton.Resources;
4 |
5 | #region sample_IStatefulResourceSource
6 |
7 | ///
8 | /// Expose multiple stateful resources
9 | ///
10 | public interface IStatefulResourceSource
11 | {
12 | IReadOnlyList FindResources();
13 | }
14 |
15 | #endregion
--------------------------------------------------------------------------------
/src/Oakton/Resources/ResourceAction.cs:
--------------------------------------------------------------------------------
1 | namespace Oakton.Resources;
2 |
3 | public enum ResourceAction
4 | {
5 | clear,
6 | teardown,
7 | setup,
8 | statistics,
9 | check,
10 | list
11 | }
--------------------------------------------------------------------------------
/src/Oakton/Resources/ResourceEnvironmentCheck.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Oakton.Environment;
5 |
6 | namespace Oakton.Resources;
7 |
8 | internal class ResourceEnvironmentCheck : IEnvironmentCheck
9 | {
10 | private readonly IStatefulResource _resource;
11 |
12 | public ResourceEnvironmentCheck(IStatefulResource resource)
13 | {
14 | _resource = resource;
15 | }
16 |
17 | public string Description => $"Resource {_resource.Name} ({_resource.Type})";
18 |
19 | public Task Assert(IServiceProvider services, CancellationToken cancellation)
20 | {
21 | return _resource.Check(cancellation);
22 | }
23 | }
--------------------------------------------------------------------------------
/src/Oakton/Resources/ResourceInput.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace Oakton.Resources;
5 |
6 | public class ResourceInput : NetCoreInput
7 | {
8 | private readonly Lazy _cancellation;
9 |
10 | public ResourceInput()
11 | {
12 | _cancellation =
13 | new Lazy(() => new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutFlag)));
14 | }
15 |
16 | [Description("Resource action, default is setup")]
17 | public ResourceAction Action { get; set; } = ResourceAction.setup;
18 |
19 | [Description("Timeout in seconds, default is 60")]
20 | public int TimeoutFlag { get; set; } = 60;
21 |
22 | [IgnoreOnCommandLine] public CancellationTokenSource TokenSource => _cancellation.Value;
23 |
24 | [Description("Optionally filter by resource type")]
25 | public string TypeFlag { get; set; }
26 |
27 | [Description("Optionally filter by resource name")]
28 | public string NameFlag { get; set; }
29 | }
--------------------------------------------------------------------------------
/src/Oakton/Resources/ResourceSetupException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Oakton.Resources;
4 |
5 | public class ResourceSetupException : Exception
6 | {
7 | public ResourceSetupException(IStatefulResource resource, Exception ex) : base(
8 | $"Failed to setup resource {resource.Name} of type {resource.Type}", ex)
9 |
10 | {
11 | }
12 |
13 | public ResourceSetupException(IStatefulResourceSource source, Exception ex) : base(
14 | $"Failed to execute resource source {source}", ex)
15 | {
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Oakton/Resources/ResourceSetupHostService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.Extensions.Hosting;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace Oakton.Resources;
10 |
11 | internal class ResourceSetupOptions
12 | {
13 | public StartupAction Action { get; set; } = StartupAction.SetupOnly;
14 | }
15 |
16 | internal class ResourceSetupHostService : IHostedService
17 | {
18 | private readonly ILogger _logger;
19 | private readonly ResourceSetupOptions _options;
20 | private readonly IStatefulResource[] _resources;
21 | private readonly IStatefulResourceSource[] _sources;
22 |
23 | public ResourceSetupHostService(ResourceSetupOptions options, IEnumerable resources,
24 | IEnumerable sources, ILogger logger)
25 | {
26 | _resources = resources.ToArray();
27 | _sources = sources.ToArray();
28 | _options = options;
29 | _logger = logger;
30 | }
31 |
32 | public async Task StartAsync(CancellationToken cancellationToken)
33 | {
34 | var list = new List();
35 | var resources = new List(_resources);
36 |
37 | foreach (var source in _sources)
38 | {
39 | try
40 | {
41 | resources.AddRange(source.FindResources());
42 | }
43 | catch (Exception e)
44 | {
45 | _logger.LogError(e, "Failed to find resource sources from {Source}", source);
46 | list.Add(new ResourceSetupException(source, e));
47 | }
48 | }
49 |
50 | async ValueTask execute(IStatefulResource r, CancellationToken t)
51 | {
52 | try
53 | {
54 | await r.Setup(cancellationToken).ConfigureAwait(false);
55 | _logger.LogInformation("Ran Setup() on resource {Name} of type {Type}", r.Name, r.Type);
56 |
57 | if (_options.Action == StartupAction.ResetState)
58 | {
59 | await r.ClearState(cancellationToken).ConfigureAwait(false);
60 | _logger.LogInformation("Ran ClearState() on resource {Name} of type {Type}", r.Name, r.Type);
61 | }
62 | }
63 | catch (Exception e)
64 | {
65 | var wrapped = new ResourceSetupException(r, e);
66 | _logger.LogError(e, "Failed to setup resource {Name} of type {Type}", r.Name, r.Type);
67 |
68 | list.Add(wrapped);
69 | }
70 | }
71 |
72 | foreach (var resource in resources) await execute(resource, cancellationToken).ConfigureAwait(false);
73 |
74 | if (list.Any())
75 | {
76 | throw new AggregateException(list);
77 | }
78 | }
79 |
80 | public Task StopAsync(CancellationToken cancellationToken)
81 | {
82 | return Task.CompletedTask;
83 | }
84 | }
--------------------------------------------------------------------------------
/src/Oakton/Resources/StatefulResourceBase.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using Spectre.Console;
4 | using Spectre.Console.Rendering;
5 |
6 | namespace Oakton.Resources;
7 |
8 | ///
9 | /// Base class with empty implementations for IStatefulResource.
10 | ///
11 | public abstract class StatefulResourceBase : IStatefulResource
12 | {
13 | protected StatefulResourceBase(string type, string name)
14 | {
15 | Type = type;
16 | Name = name;
17 | }
18 |
19 | public virtual Task Check(CancellationToken token)
20 | {
21 | return Task.CompletedTask;
22 | }
23 |
24 | public virtual Task ClearState(CancellationToken token)
25 | {
26 | return Task.CompletedTask;
27 | }
28 |
29 | public virtual Task Teardown(CancellationToken token)
30 | {
31 | return Task.CompletedTask;
32 | }
33 |
34 | public virtual Task Setup(CancellationToken token)
35 | {
36 | return Task.CompletedTask;
37 | }
38 |
39 | public virtual Task DetermineStatus(CancellationToken token)
40 | {
41 | return Task.FromResult((IRenderable)new Markup("Okay"));
42 | }
43 |
44 | public string Type { get; }
45 | public string Name { get; }
46 | }
--------------------------------------------------------------------------------
/src/Oakton/jasper-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasperFx/oakton/3aec61cb8641d903cea76e7ba21176c2083249ae/src/Oakton/jasper-icon.png
--------------------------------------------------------------------------------
/src/OaktonSample/OaktonSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net7.0
4 | OaktonSample
5 | Exe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/OaktonSample/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "OaktonSample": {
4 | "commandName": "Project",
5 | "commandLineArgs": "help"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/OptsFileUsageDemo/HelloCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Oakton;
3 |
4 | namespace OptsFileUsageDemo
5 | {
6 | public class HelloInput
7 | {
8 | public int Times { get; set; }
9 | }
10 |
11 | public class HelloCommand : OaktonCommand
12 | {
13 | public override bool Execute(HelloInput input)
14 | {
15 | for (var i = 0; i < input.Times; i++)
16 | {
17 | Console.WriteLine($"{i + 1}. Hello!");
18 | }
19 |
20 | Console.WriteLine("Press any key to end.");
21 | Console.ReadLine();
22 |
23 | return true;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/OptsFileUsageDemo/OptsFileUsageDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | PreserveNewest
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/OptsFileUsageDemo/Program.cs:
--------------------------------------------------------------------------------
1 | // See https://aka.ms/new-console-template for more information
2 |
3 | using Microsoft.Extensions.Hosting;
4 | using Oakton;
5 |
6 | return Host.CreateDefaultBuilder(args)
7 | .RunOaktonCommandsSynchronously(args, "sample.opts");
--------------------------------------------------------------------------------
/src/OptsFileUsageDemo/sample.opts:
--------------------------------------------------------------------------------
1 | hello 5
--------------------------------------------------------------------------------
/src/Tests/ActivatorCommandCreatorTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Oakton;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests
7 | {
8 | public class ActivatorCommandCreatorTests
9 | {
10 | [Fact]
11 | public void builds_an_instance_with_no_ctor_parameters()
12 | {
13 | var creator = new ActivatorCommandCreator();
14 | var instance = creator.CreateCommand(typeof (NoParamsCommand));
15 |
16 | instance.ShouldBeOfType();
17 | }
18 |
19 | [Fact]
20 | public void throws_if_the_ctor_has_parameters()
21 | {
22 | var creator = new ActivatorCommandCreator();
23 |
24 | Assert.Throws(() => creator.CreateCommand(typeof (ParamsCommand)));
25 | }
26 |
27 | public class FakeModel
28 | {
29 | }
30 |
31 | private class NoParamsCommand : OaktonCommand
32 | {
33 | public override bool Execute(FakeModel input)
34 | {
35 | throw new System.NotImplementedException();
36 | }
37 | }
38 |
39 | private class ParamsCommand : OaktonCommand
40 | {
41 | public ParamsCommand(string testArgument)
42 | {
43 | }
44 |
45 | public override bool Execute(FakeModel input)
46 | {
47 | throw new System.NotImplementedException();
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Tests/ArgPreprocessorTester.cs:
--------------------------------------------------------------------------------
1 | using Oakton.Parsing;
2 | using Xunit;
3 |
4 | namespace Tests
5 | {
6 |
7 | public class ArgPreprocessorTester
8 | {
9 | [Fact]
10 | public void should_split_multi_args()
11 | {
12 | ArgPreprocessor.Process(new[] {"-abc"}).ShouldHaveTheSameElementsAs("-a", "-b", "-c");
13 | }
14 |
15 | [Fact]
16 | public void combined_short_flags_should_be_case_sensitive()
17 | {
18 | ArgPreprocessor.Process(new[] { "-aAbBcC" }).ShouldHaveTheSameElementsAs("-a","-A", "-b","-B", "-c","-C");
19 | }
20 |
21 | [Fact]
22 | public void should_ignore_long_flag_args()
23 | {
24 | ArgPreprocessor.Process(new[] {"--abc"}).ShouldHaveTheSameElementsAs("--abc");
25 | }
26 |
27 | [Fact]
28 | public void should_support_multiple_types_of_flags()
29 | {
30 | ArgPreprocessor.Process(new[] { "-abc", "--xyz", "b" }).ShouldHaveTheSameElementsAs("-a", "-b", "-c", "--xyz", "b");
31 | }
32 |
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Tests/ArgumentTester.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using JasperFx.Core.Reflection;
4 | using Oakton;
5 | using Oakton.Internal.Conversion;
6 | using Shouldly;
7 | using Xunit;
8 |
9 | namespace Tests
10 | {
11 |
12 | public class ArgumentTester
13 | {
14 | private Argument argFor(Expression> property)
15 | {
16 | return new Argument(ReflectionHelper.GetProperty(property), new Conversions());
17 | }
18 |
19 | [Fact]
20 | public void description_is_just_the_property_name_if_no_description_attribute()
21 | {
22 | argFor(x => x.Name).Description.ShouldBe("Name");
23 | }
24 |
25 | [Fact]
26 | public void description_comes_from_the_attribute_if_it_exists()
27 | {
28 | argFor(x => x.Age).Description.ShouldBe("age of target");
29 | }
30 |
31 | [Fact]
32 | public void to_usage_description_with_a_simple_string_or_number_type()
33 | {
34 | argFor(x => x.Name).ToUsageDescription().ShouldBe("");
35 | argFor(x => x.Age).ToUsageDescription().ShouldBe("");
36 | }
37 |
38 | [Fact]
39 | public void to_usage_description_with_an_enumeration()
40 | {
41 | argFor(x => x.Enum).ToUsageDescription().ShouldBe("red|blue|green");
42 | }
43 |
44 |
45 | }
46 |
47 | public enum TargetEnum
48 | {
49 | red,blue,green
50 | }
51 |
52 | public class ArgumentTarget
53 | {
54 | public string Name { get; set; }
55 |
56 | public TargetEnum Enum{ get; set;}
57 |
58 | [Description("age of target")]
59 | public int Age { get; set; }
60 |
61 | }
62 | }
--------------------------------------------------------------------------------
/src/Tests/BooleanFlagTester.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using JasperFx.Core.Reflection;
4 | using Oakton;
5 | using Oakton.Parsing;
6 | using Shouldly;
7 | using Xunit;
8 |
9 | namespace Tests
10 | {
11 |
12 | public class BooleanFlagTester
13 | {
14 |
15 | private BooleanFlag getFlag(Expression> expression)
16 | {
17 | return new BooleanFlag(ReflectionHelper.GetProperty(expression));
18 | }
19 |
20 |
21 | [Fact]
22 | public void get_usage_description_with_an_alias()
23 | {
24 | getFlag(x => x.AliasedFlag).ToUsageDescription().ShouldBe("[-a, --aliased]");
25 | }
26 |
27 | [Fact]
28 | public void get_usage_description_without_an_alias()
29 | {
30 | getFlag(x => x.NormalFlag).ToUsageDescription().ShouldBe("[-n, --normal]");
31 | }
32 | }
33 |
34 | public class BooleanFlagTarget
35 | {
36 | [FlagAlias("aliased", 'a')]
37 | public bool AliasedFlag { get; set; }
38 | public bool NormalFlag { get; set; }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Tests/Bugs/bug_24_negative_numbers.cs:
--------------------------------------------------------------------------------
1 | using Oakton;
2 | using Shouldly;
3 | using Xunit;
4 |
5 | namespace Tests.Bugs
6 | {
7 | public class bug_24_negative_numbers
8 | {
9 | [Description("Test")]
10 | public class MyInput
11 | {
12 | public int ArgNum { get; set; }
13 |
14 | public int NumFlag { get; set; }
15 | }
16 |
17 | class MyCommand : OaktonCommand
18 | {
19 |
20 | public override bool Execute(MyInput input)
21 | {
22 | return true;
23 | }
24 | }
25 |
26 | [Fact]
27 | public void should_allow_negative_numbers_in_arguments()
28 | {
29 | var factory = new CommandFactory();
30 | factory.RegisterCommand();
31 |
32 | factory.BuildRun("my \"-3\"")
33 | .Input.ShouldBeOfType()
34 | .ArgNum.ShouldBe(-3);
35 | }
36 |
37 | [Fact]
38 | public void should_allow_negative_numbes_in_flag()
39 | {
40 | var factory = new CommandFactory();
41 | factory.RegisterCommand();
42 |
43 | factory.BuildRun("my \"-3\" --num \"-5\"")
44 | .Input.ShouldBeOfType()
45 | .NumFlag.ShouldBe(-5);
46 | }
47 |
48 | [Theory]
49 | [InlineData(1, 2)]
50 | [InlineData(10, 20)]
51 | [InlineData(1, -2)]
52 | [InlineData(1, -20)]
53 | [InlineData(-1, 2)]
54 | [InlineData(-1, -20)]
55 | [InlineData(-10, -20)]
56 | public void ShouldBeAbleToParseAllNumbersWithQuotes(int argVal, int optVal)
57 | {
58 | var cmd = $"my \"{argVal}\" --num \"{optVal}\"";
59 |
60 | var f = new CommandFactory();
61 | f.RegisterCommand();
62 |
63 | var runner = f.BuildRun(cmd);
64 |
65 | var input = runner.Input.ShouldBeOfType();
66 |
67 | input.ArgNum.ShouldBe(argVal);
68 | input.NumFlag.ShouldBe(optVal);
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/Tests/Conversion/DateTime_conversion_specs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Oakton.Internal.Conversion;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests.Conversion
7 | {
8 | public class DateTime_conversion_specs
9 | {
10 | [Fact]
11 | public void get_date_time_for_day_and_time()
12 | {
13 | var date = DateTimeConverter.GetDateTime("Saturday 14:30");
14 |
15 | date.DayOfWeek.ShouldBe(DayOfWeek.Saturday);
16 | date.Date.AddHours(14).AddMinutes(30).ShouldBe(date);
17 | (date >= DateTime.Today).ShouldBe(true);
18 | }
19 |
20 | [Fact]
21 | public void get_date_time_for_day_and_time_2()
22 | {
23 | var date = DateTimeConverter.GetDateTime("Monday 14:30");
24 |
25 | date.DayOfWeek.ShouldBe(DayOfWeek.Monday);
26 | date.Date.AddHours(14).AddMinutes(30).ShouldBe(date);
27 | (date >= DateTime.Today).ShouldBe(true);
28 | }
29 |
30 | [Fact]
31 | public void get_date_time_for_day_and_time_3()
32 | {
33 | var date = DateTimeConverter.GetDateTime("Wednesday 14:30");
34 |
35 | date.DayOfWeek.ShouldBe(DayOfWeek.Wednesday);
36 | date.Date.AddHours(14).AddMinutes(30).ShouldBe(date);
37 | (date >= DateTime.Today).ShouldBe(true);
38 | }
39 |
40 | [Fact]
41 | public void get_date_time_from_full_iso_8601_should_be_a_utc_datetime()
42 | {
43 | var date = DateTimeConverter.GetDateTime("2012-06-01T14:52:35.0000000Z");
44 |
45 | date.ShouldBe(new DateTime(2012, 06, 01, 14, 52, 35, DateTimeKind.Utc));
46 | }
47 |
48 | [Fact]
49 | public void get_date_time_from_partial_iso_8601_uses_default_parser_and_is_local()
50 | {
51 | var date = DateTimeConverter.GetDateTime("2012-06-01T12:52:35Z");
52 |
53 | var gmtOffsetInHours = TimeZoneInfo.Local.GetUtcOffset(date).TotalHours;
54 | date.ShouldBe(new DateTime(2012, 06, 01, 12, 52, 35, DateTimeKind.Local).AddHours(gmtOffsetInHours));
55 | }
56 |
57 | [Fact]
58 | public void get_date_time_from_24_hour_time()
59 | {
60 | DateTimeConverter.GetDateTime("14:30").ShouldBe(DateTime.Today.AddHours(14).AddMinutes(30));
61 | }
62 |
63 | [Fact]
64 | public void parse_today()
65 | {
66 | DateTimeConverter.GetDateTime("TODAY").ShouldBe(DateTime.Today);
67 | }
68 |
69 | [Fact]
70 | public void parse_today_minus_date()
71 | {
72 | DateTimeConverter.GetDateTime("TODAY-3").ShouldBe(DateTime.Today.AddDays(-3));
73 | }
74 |
75 | [Fact]
76 | public void parse_today_plus_date()
77 | {
78 | DateTimeConverter.GetDateTime("TODAY+5").ShouldBe(DateTime.Today.AddDays(5));
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/src/Tests/Conversion/StringConverterProviderTester.cs:
--------------------------------------------------------------------------------
1 | using Oakton.Internal.Conversion;
2 | using Shouldly;
3 | using Xunit;
4 |
5 | namespace Tests.Conversion
6 | {
7 | public class StringConstrucutorConversionProviderTester
8 | {
9 | private readonly StringConverterProvider provider = new StringConverterProvider();
10 |
11 | [Fact]
12 | public void provide_instance_with_string_constructor()
13 | {
14 | var @object = provider.ConverterFor(typeof (TestKlass));
15 | @object.ShouldNotBeNull();
16 |
17 | var result = @object("Sample");
18 |
19 | result.ShouldNotBeNull();
20 | result.ShouldBeOfType()
21 | .S.ShouldBe("Sample");
22 | }
23 |
24 | [Fact]
25 | public void return_null_for_invalid_class()
26 | {
27 | var @object = provider.ConverterFor(typeof (TestKlass2));
28 | @object.ShouldBeNull();
29 | }
30 |
31 |
32 | public class TestKlass
33 | {
34 | public string S;
35 |
36 | public TestKlass(string s)
37 | {
38 | S = s;
39 | }
40 | }
41 |
42 | public class TestKlass2
43 | {
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/Tests/Conversion/TimeSpanConverterTester.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Oakton.Internal.Conversion;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests.Conversion
7 | {
8 | public class TimeSpanConverterTester
9 | {
10 | [Fact]
11 | public void happily_converts_timespans_in_4_digit_format()
12 | {
13 | TimeSpanConverter.GetTimeSpan("1230").ShouldBe(new TimeSpan(12, 30, 0));
14 | }
15 |
16 | [Fact]
17 | public void happily_converts_timespans_in_5_digit_format()
18 | {
19 | TimeSpanConverter.GetTimeSpan("12:30").ShouldBe(new TimeSpan(12, 30, 0));
20 | }
21 |
22 | [Fact]
23 | public void converts_timespans_for_seconds()
24 | {
25 | TimeSpanConverter.GetTimeSpan("3.5s").ShouldBe(TimeSpan.FromSeconds(3.5));
26 | TimeSpanConverter.GetTimeSpan("5 s").ShouldBe(TimeSpan.FromSeconds(5));
27 | TimeSpanConverter.GetTimeSpan("1 second").ShouldBe(TimeSpan.FromSeconds(1));
28 | TimeSpanConverter.GetTimeSpan("12 seconds").ShouldBe(TimeSpan.FromSeconds(12));
29 | }
30 |
31 | [Fact]
32 | public void converts_timespans_for_minutes()
33 | {
34 | TimeSpanConverter.GetTimeSpan("10m").ShouldBe(TimeSpan.FromMinutes(10));
35 | TimeSpanConverter.GetTimeSpan("2.1 m").ShouldBe(TimeSpan.FromMinutes(2.1));
36 | TimeSpanConverter.GetTimeSpan("1 minute").ShouldBe(TimeSpan.FromMinutes(1));
37 | TimeSpanConverter.GetTimeSpan("5 minutes").ShouldBe(TimeSpan.FromMinutes(5));
38 | }
39 |
40 | [Fact]
41 | public void converts_timespans_for_hours()
42 | {
43 | TimeSpanConverter.GetTimeSpan("24h").ShouldBe(TimeSpan.FromHours(24));
44 | TimeSpanConverter.GetTimeSpan("4 h").ShouldBe(TimeSpan.FromHours(4));
45 | TimeSpanConverter.GetTimeSpan("1 hour").ShouldBe(TimeSpan.FromHours(1));
46 | TimeSpanConverter.GetTimeSpan("12.5 hours").ShouldBe(TimeSpan.FromHours(12.5));
47 | }
48 |
49 | [Fact]
50 | public void converts_timespans_for_days()
51 | {
52 | TimeSpanConverter.GetTimeSpan("3d").ShouldBe(TimeSpan.FromDays(3));
53 | TimeSpanConverter.GetTimeSpan("2 d").ShouldBe(TimeSpan.FromDays(2));
54 | TimeSpanConverter.GetTimeSpan("1 day").ShouldBe(TimeSpan.FromDays(1));
55 | TimeSpanConverter.GetTimeSpan("7 days").ShouldBe(TimeSpan.FromDays(7));
56 | }
57 |
58 | [Fact]
59 | public void can_convert_from_standard_format()
60 | {
61 | TimeSpanConverter.GetTimeSpan("00:00:01").ShouldBe(new TimeSpan(0, 0, 1));
62 | TimeSpanConverter.GetTimeSpan("00:10:00").ShouldBe(new TimeSpan(0, 10, 0));
63 | TimeSpanConverter.GetTimeSpan("01:30:00").ShouldBe(new TimeSpan(1, 30, 0));
64 | TimeSpanConverter.GetTimeSpan("1.01:30:00").ShouldBe(new TimeSpan(1, 1, 30, 0));
65 | TimeSpanConverter.GetTimeSpan("-00:10:00").ShouldBe(new TimeSpan(0, -10, 0));
66 | TimeSpanConverter.GetTimeSpan("12:34:56.789").ShouldBe(new TimeSpan(0, 12, 34, 56, 789));
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/src/Tests/CustomCommandCreatorTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Oakton;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests
7 | {
8 | public class CustomCommandCreatorTests
9 | {
10 | [Fact]
11 | public void command_factory_creates_with_custom_creator()
12 | {
13 | TestCommandCreator creator = new TestCommandCreator();
14 | ICommandFactory factory = new CommandFactory(creator);
15 | var executor = CommandExecutor.For(_ =>
16 | {
17 | _.RegisterCommand();
18 | }, creator);
19 |
20 | executor.Execute("test");
21 |
22 | creator.LastCreatedModel.ShouldNotBeNull();
23 | }
24 |
25 | [Fact]
26 | public void help_displays_with_custom_creator()
27 | {
28 | TestCommandCreator creator = new TestCommandCreator();
29 | CommandFactory factory = new CommandFactory(creator);
30 | var executor = CommandExecutor.For(_ =>
31 | {
32 | _.RegisterCommand();
33 | }, creator);
34 |
35 | executor.Execute("");
36 |
37 | creator.LastCreatedModel.ShouldNotBeNull();
38 | }
39 |
40 | private class TestCommand : OaktonCommand
41 | {
42 | public TestCommand(string message)
43 | {
44 | Message = message;
45 | }
46 |
47 | public string Message { get; }
48 |
49 | public override bool Execute(object input)
50 | {
51 | return true;
52 | }
53 | }
54 |
55 | private class TestOptions
56 | {
57 | public TestOptions(string message)
58 | {
59 | Message = message;
60 | }
61 |
62 | public string Message { get; }
63 | }
64 |
65 | private class TestCommandCreator : ICommandCreator
66 | {
67 | public TestCommand LastCreatedCommand { get; private set; }
68 |
69 | public TestOptions LastCreatedModel { get; private set; }
70 |
71 | public IOaktonCommand CreateCommand(Type commandType)
72 | {
73 | LastCreatedCommand = new TestCommand("created command");
74 | return LastCreatedCommand;
75 | }
76 |
77 | public object CreateModel(Type modelType)
78 | {
79 | LastCreatedModel = new TestOptions("created options");
80 | return LastCreatedModel;
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Tests/Descriptions/ConfigurationPreviewTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 | using JasperFx.Core;
6 | using Microsoft.Extensions.Configuration;
7 | using Oakton.Descriptions;
8 | using Xunit;
9 |
10 | namespace Tests.Descriptions;
11 |
12 | [Collection("SetConsoleOutput")]
13 | public class ConfigurationPreviewTests
14 | {
15 | [Fact]
16 | public async Task write_configuration_value_with_bracket_to_console()
17 | {
18 | // Arrange
19 | var configBuilder = new ConfigurationBuilder();
20 | configBuilder.AddInMemoryCollection(new Dictionary
21 | {
22 | { "NoBracketKey", "hello world" },
23 | { "BracketKey", "value with bracket [hello]" }
24 | });
25 | var config = configBuilder.Build();
26 |
27 |
28 | var original = Console.Out;
29 | var output = new StringWriter();
30 | Console.SetOut(output);
31 |
32 | try
33 | {
34 | // Act
35 | var preview = new ConfigurationPreview(config);
36 | await preview.WriteToConsole();
37 |
38 |
39 | // Assert
40 | var text = output.ToString().ReadLines();
41 |
42 | }
43 | finally
44 | {
45 | Console.SetOut(original);
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Tests/DictionaryFlagTester.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Reflection;
3 | using Oakton;
4 | using Shouldly;
5 | using Xunit;
6 |
7 | namespace Tests
8 | {
9 | public class DictionaryFlagTester
10 | {
11 | private CommandFactory theFactory;
12 |
13 | public DictionaryFlagTester()
14 | {
15 | theFactory = new CommandFactory();
16 | theFactory.RegisterCommands(GetType().GetTypeInfo().Assembly);
17 | }
18 |
19 | private CommandRun forArgs(string args)
20 | {
21 | return theFactory.BuildRun(args);
22 | }
23 |
24 | [Fact]
25 | public void use_prop_flags()
26 | {
27 | var run = forArgs("dict --prop:color red --prop:age 43");
28 |
29 | var input = run.Input.ShouldBeOfType();
30 | input.PropFlag["color"].ShouldBe("red");
31 | input.PropFlag["age"].ShouldBe("43");
32 | }
33 |
34 | [Fact]
35 | public void use_prop_flags_when_dict_has_to_be_built()
36 | {
37 | var run = forArgs("missingdict --prop:color red --prop:age 43");
38 |
39 | var input = run.Input.ShouldBeOfType();
40 | input.PropFlag["color"].ShouldBe("red");
41 | input.PropFlag["age"].ShouldBe("43");
42 | }
43 | }
44 |
45 | #region sample_DictInput
46 | public class DictInput
47 | {
48 | public Dictionary PropFlag = new Dictionary();
49 | }
50 | #endregion
51 |
52 | public class DictCommand : OaktonCommand
53 | {
54 | public override bool Execute(DictInput input)
55 | {
56 | return true;
57 | }
58 | }
59 |
60 | public class MissingInput
61 | {
62 | public Dictionary PropFlag;
63 | }
64 |
65 | [Description("SOMETHING", Name = "missingdict")]
66 | public class MissingDictCommand : OaktonCommand
67 | {
68 | public override bool Execute(MissingInput input)
69 | {
70 | return true;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Tests/EnumerableArgumentTester.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq.Expressions;
4 | using JasperFx.Core.Reflection;
5 | using Oakton;
6 | using Oakton.Internal.Conversion;
7 | using Oakton.Parsing;
8 | using Shouldly;
9 | using Xunit;
10 |
11 | namespace Tests
12 | {
13 |
14 | public class EnumerableArgumentTester
15 | {
16 | private EnumerableArgument argFor(Expression> property)
17 | {
18 | return new EnumerableArgument(ReflectionHelper.GetProperty(property), new Conversions());
19 | }
20 |
21 | [Fact]
22 | public void description_is_just_the_property_name_if_no_description_attribute()
23 | {
24 | argFor(x => x.Names).Description.ShouldBe("Names");
25 | }
26 |
27 | [Fact]
28 | public void description_comes_from_the_attribute_if_it_exists()
29 | {
30 | argFor(x => x.Ages).Description.ShouldBe("ages of target");
31 | }
32 |
33 | [Fact]
34 | public void to_usage_description_with_a_simple_string_or_number_type()
35 | {
36 | argFor(x => x.Names).ToUsageDescription().ShouldBe("");
37 | argFor(x => x.Ages).ToUsageDescription().ShouldBe("");
38 | }
39 |
40 | [Fact]
41 | public void x()
42 | {
43 | Console.WriteLine(argFor(x => x.OptionalFlag).ToUsageDescription());
44 |
45 | }
46 |
47 | [Fact]
48 | public void handle()
49 | {
50 | var target = new EnumerableArgumentInput();
51 | var queue = new Queue();
52 | queue.Enqueue("a");
53 | queue.Enqueue("b");
54 | queue.Enqueue("c");
55 |
56 | argFor(x => x.Names).Handle(target, queue);
57 |
58 | target.Names.ShouldHaveTheSameElementsAs("a", "b", "c");
59 | }
60 |
61 | }
62 |
63 | #region sample_EnumerableArguments
64 | public class EnumerableArgumentInput
65 | {
66 | public IEnumerable Names { get; set; }
67 |
68 | public IEnumerable OptionalFlag { get; set; }
69 |
70 | public IEnumerable Enums { get; set; }
71 |
72 | [Description("ages of target")]
73 | public IEnumerable Ages { get; set; }
74 |
75 | }
76 | #endregion
77 | }
78 |
--------------------------------------------------------------------------------
/src/Tests/Environment/EnvironmentCheckResultsTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Oakton.Environment;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests.Environment
7 | {
8 | public class EnvironmentCheckResultsTests
9 | {
10 | [Fact]
11 | public void empty_results_asserts_just_fine()
12 | {
13 | new EnvironmentCheckResults().Assert();
14 | }
15 |
16 | [Fact]
17 | public void assert_with_only_successes()
18 | {
19 | var checkResults = new EnvironmentCheckResults();
20 | checkResults.RegisterSuccess("Okay");
21 | checkResults.RegisterSuccess("Still Okay");
22 |
23 | checkResults.Assert();
24 | }
25 |
26 | [Fact]
27 | public void assert_with_failures()
28 | {
29 | var checkResults = new EnvironmentCheckResults();
30 |
31 | checkResults.RegisterSuccess("Okay");
32 |
33 | checkResults.RegisterSuccess("Still Okay");
34 |
35 | checkResults.RegisterFailure("bad!", new DivideByZeroException());
36 |
37 | var ex = Should.Throw(() => checkResults.Assert());
38 |
39 | ex.Results.ShouldBeSameAs(checkResults);
40 | }
41 |
42 | [Fact]
43 | public void succeeded()
44 | {
45 | var checkResults = new EnvironmentCheckResults();
46 | checkResults.Succeeded().ShouldBeTrue();
47 |
48 | checkResults.RegisterSuccess("Okay");
49 | checkResults.Succeeded().ShouldBeTrue();
50 |
51 | checkResults.RegisterSuccess("Still Okay");
52 | checkResults.Succeeded().ShouldBeTrue();
53 |
54 | checkResults.RegisterFailure("bad!", new DivideByZeroException());
55 | checkResults.Succeeded().ShouldBeFalse();
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Tests/Environment/LambdaCheckTests.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using Oakton.Environment;
4 | using Shouldly;
5 | using Xunit;
6 |
7 | namespace Tests.Environment
8 | {
9 | public class LambdaCheckTests
10 | {
11 | [Fact]
12 | public void description()
13 | {
14 | var check = new LambdaCheck("it's okay", (s, t) => Task.CompletedTask);
15 | check.Description.ShouldBe("it's okay");
16 | }
17 |
18 | [Fact]
19 | public async Task call_the_assert()
20 | {
21 | bool wasCalled = false;
22 | var check = new LambdaCheck("it's okay", (s, t) =>
23 | {
24 | wasCalled = true;
25 | return Task.CompletedTask;
26 | });
27 |
28 | await check.Assert(null, default(CancellationToken));
29 |
30 | wasCalled.ShouldBeTrue();
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Tests/HostedCommandsTester.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Oakton;
4 | using System;
5 | using Xunit;
6 |
7 | namespace Tests;
8 |
9 | public class HostedCommandsTester
10 | {
11 | [Fact]
12 | public void CanInjectServicesIntoCommands()
13 | {
14 | var builder = Host.CreateDefaultBuilder()
15 | .ConfigureServices(services =>
16 | {
17 | services.AddScoped();
18 | services.AddOakton(options =>
19 | {
20 | options.Factory = factory =>
21 | {
22 | factory.RegisterCommand();
23 | };
24 | options.DefaultCommand = "TestDI";
25 | });
26 | });
27 |
28 | var app = builder.Build();
29 |
30 | app.RunHostedOaktonCommands(Array.Empty());
31 |
32 | Assert.Equal(1, TestDICommand.Value);
33 | }
34 |
35 | public class TestInput
36 | {
37 | }
38 |
39 | public class TestDependency : IDisposable
40 | {
41 | public int Value { get; private set; }
42 |
43 | public TestDependency()
44 | {
45 | Value = 1;
46 | }
47 |
48 | public void Dispose()
49 | {
50 | Value = 0;
51 | GC.SuppressFinalize(this);
52 | }
53 | }
54 |
55 | public class TestDICommand : OaktonCommand
56 | {
57 | public static int Value { get; set; } = 0;
58 | private readonly TestDependency _dep;
59 | public TestDICommand(TestDependency dep)
60 | {
61 | _dep = dep;
62 | }
63 |
64 | public override bool Execute(TestInput input)
65 | {
66 | Value = _dep.Value;
67 | return true;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Tests/NoXUnitParallelization.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
4 |
--------------------------------------------------------------------------------
/src/Tests/OptionReaderTester.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using JasperFx.Core;
4 | using Oakton.Parsing;
5 | using Shouldly;
6 | using Xunit;
7 |
8 | namespace Tests
9 | {
10 | public class OptionReaderTester
11 | {
12 | #if NET451
13 | private string directory = AppDomain.CurrentDomain.BaseDirectory;
14 | #else
15 | private string directory = AppContext.BaseDirectory;
16 | #endif
17 |
18 | [Fact]
19 | public void read_from_one_line()
20 | {
21 | var path = directory.AppendPath("opts1.txt");
22 | File.WriteAllText(path, "-f -a -b");
23 |
24 | OptionReader.Read(path)
25 | .ShouldBe("-f -a -b");
26 | }
27 |
28 | [Fact]
29 | public void read_from_multiple_lines()
30 | {
31 | var path = directory.AppendPath("opts2.txt");
32 |
33 | using (var stream = new FileStream(path, FileMode.Create))
34 | {
35 | var writer = new StreamWriter(stream);
36 |
37 | writer.WriteLine("--color Blue");
38 | writer.WriteLine("--size Medium ");
39 | writer.WriteLine(" --direction East");
40 |
41 | writer.Flush();
42 |
43 | writer.Dispose();
44 | }
45 |
46 | OptionReader.Read(path)
47 | .ShouldBe("--color Blue --size Medium --direction East");
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/Tests/OptionsSamples.cs:
--------------------------------------------------------------------------------
1 | using Oakton;
2 |
3 | namespace Tests
4 | {
5 | public class OptionsSamples
6 | {
7 | public static void go()
8 | {
9 | #region sample_configuring_opts_file
10 | var executor = CommandExecutor.For(_ =>
11 | {
12 | // configure the command discovery
13 | });
14 |
15 | executor.OptionsFile = "mytool.opts";
16 | #endregion
17 | }
18 | }
19 |
20 | #region sample_SecuredInput
21 | public class SecuredInput
22 | {
23 | public string UserName { get; set; }
24 | public string Password { get; set; }
25 | }
26 | #endregion
27 | }
28 |
--------------------------------------------------------------------------------
/src/Tests/Resources/resource_filtering.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Threading.Tasks;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests.Resources
7 | {
8 | public class resource_filtering : ResourceCommandContext
9 | {
10 | [Fact]
11 | public void uses_resource_source()
12 | {
13 | var blue = AddResource("blue", "color");
14 | var red = AddResource("red", "color");
15 |
16 | AddSource(col =>
17 | {
18 | col.Add("purple", "color");
19 | col.Add("orange", "color");
20 | });
21 |
22 | AddSource(col =>
23 | {
24 | col.Add("green", "color");
25 | col.Add("white", "color");
26 | });
27 |
28 | var resources = applyTheResourceFiltering();
29 |
30 | var colors = resources.Select(x => x.Name).OrderBy(x => x)
31 | .ToList();
32 |
33 | colors.ShouldHaveTheSameElementsAs("blue", "green", "orange", "purple", "red", "white");
34 | }
35 |
36 | [Fact]
37 | public void no_filtering()
38 | {
39 | var blue = AddResource("blue", "color");
40 | var red = AddResource("red", "color");
41 |
42 | var tx = AddResource("tx", "state");
43 | var ar = AddResource("ar", "state");
44 |
45 | var resources = applyTheResourceFiltering();
46 |
47 | resources.Count.ShouldBe(4);
48 |
49 | resources.ShouldContain(blue);
50 | resources.ShouldContain(red);
51 | resources.ShouldContain(tx);
52 | resources.ShouldContain(ar);
53 | }
54 |
55 | [Fact]
56 | public void filter_by_name()
57 | {
58 | var blue = AddResource("blue", "color");
59 | var red = AddResource("red", "color");
60 |
61 | var tx = AddResource("tx", "state");
62 | var ar = AddResource("ar", "state");
63 |
64 | theInput.NameFlag = "tx";
65 |
66 | var resources = applyTheResourceFiltering();
67 | resources.Single()
68 | .ShouldBe(tx);
69 | }
70 |
71 | [Fact]
72 | public void filter_by_type()
73 | {
74 | var blue = AddResource("blue", "color");
75 | var red = AddResource("red", "color");
76 | var green = AddResource("green", "color");
77 |
78 | var tx = AddResource("tx", "state");
79 | var ar = AddResource("ar", "state");
80 | var mo = AddResource("mo", "state");
81 |
82 | theInput.TypeFlag = "color";
83 | var resources = applyTheResourceFiltering();
84 |
85 | resources.Count.ShouldBe(3);
86 | resources.ShouldContain(blue);
87 | resources.ShouldContain(red);
88 | resources.ShouldContain(green);
89 | }
90 | }
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/src/Tests/Resources/resource_ordering.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Shouldly;
3 | using Xunit;
4 |
5 | namespace Tests.Resources;
6 |
7 | public class resource_ordering : ResourceCommandContext
8 | {
9 | [Fact]
10 | public void respect_ordering_by_dependencies()
11 | {
12 | var one = AddResourceWithDependencies("one", "system", "blue", "red");
13 | var two = AddResourceWithDependencies("two", "system", "blue", "red", "one");
14 |
15 | var blue = AddResource("blue", "color");
16 | var red = AddResource("red", "color");
17 |
18 | var tx = AddResource("tx", "state");
19 | var ar = AddResource("ar", "state");
20 |
21 | var resources = applyTheResourceFiltering();
22 |
23 | resources.Count.ShouldBe(6);
24 |
25 | resources.Last().ShouldBe(two);
26 |
27 | resources.Select(x => x.Name).ShouldBe(new string[] { "blue", "red", "ar", "tx", "one", "two" });
28 |
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Tests/SpecificationExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using Shouldly;
7 |
8 | namespace Tests
9 | {
10 | public static class SpecificationExtensions
11 | {
12 | public static void ShouldHaveTheSameElementsAs(this IList actual, IList expected)
13 | {
14 | try
15 | {
16 | actual.ShouldNotBeNull();
17 | expected.ShouldNotBeNull();
18 |
19 | actual.Count.ShouldBe(expected.Count);
20 |
21 | for (int i = 0; i < actual.Count; i++)
22 | {
23 | actual[i].ShouldBe(expected[i]);
24 | }
25 | }
26 | catch (Exception)
27 | {
28 | Debug.WriteLine("Actual values were:");
29 | foreach (var x in actual)
30 | {
31 | Debug.WriteLine((object)x);
32 | }
33 | throw;
34 | }
35 | }
36 |
37 | public static void ShouldHaveTheSameElementsAs(this IEnumerable actual, params T[] expected)
38 | {
39 | ShouldHaveTheSameElementsAs(actual, (IEnumerable)expected);
40 | }
41 |
42 | public static void ShouldHaveTheSameElementsAs(this IEnumerable actual, IEnumerable expected)
43 | {
44 | IList actualList = (actual is IList) ? (IList)actual : actual.ToList();
45 | IList expectedList = (expected is IList) ? (IList)expected : expected.ToList();
46 |
47 | ShouldHaveTheSameElementsAs(actualList, expectedList);
48 | }
49 |
50 | public static void ShouldHaveTheSameElementKeysAs(this IEnumerable actual,
51 | IEnumerable expected,
52 | Func keySelector)
53 | {
54 | actual.ShouldNotBeNull();
55 | expected.ShouldNotBeNull();
56 |
57 | ELEMENT[] actualArray = actual.ToArray();
58 | object[] expectedArray = expected.Cast().ToArray();
59 |
60 | actualArray.Length.ShouldBe(expectedArray.Length);
61 |
62 | for (int i = 0; i < actual.Count(); i++)
63 | {
64 | keySelector(actualArray[i]).ShouldBe(expectedArray[i]);
65 | }
66 | }
67 |
68 | }
69 |
70 | public static class Exception where T : Exception
71 | {
72 | public static T ShouldBeThrownBy(Action action)
73 | {
74 | T exception = null;
75 |
76 | try
77 | {
78 | action();
79 | }
80 | catch (Exception e)
81 | {
82 | exception = e.ShouldBeOfType();
83 | }
84 |
85 | if (exception == null) throw new Exception("An exception was expected, but not thrown by the given action.");
86 |
87 | return exception;
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/Tests/StringTokenizerTester.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Oakton.Parsing;
3 | using Shouldly;
4 | using Xunit;
5 |
6 | namespace Tests
7 | {
8 |
9 | public class StringTokenizerTester
10 | {
11 | [Fact]
12 | public void empty_array_for_all_whitespace()
13 | {
14 | StringTokenizer.Tokenize("").Any().ShouldBeFalse();
15 | StringTokenizer.Tokenize(" ").Any().ShouldBeFalse();
16 | StringTokenizer.Tokenize("\t").Any().ShouldBeFalse();
17 | StringTokenizer.Tokenize("\n").Any().ShouldBeFalse();
18 | }
19 |
20 | [Fact]
21 | public void tokenize_simple_strings()
22 | {
23 | StringTokenizer.Tokenize("name age state").ShouldHaveTheSameElementsAs("name", "age", "state");
24 | StringTokenizer.Tokenize("name age state").ShouldHaveTheSameElementsAs("name", "age", "state");
25 | StringTokenizer.Tokenize("name age\nstate").ShouldHaveTheSameElementsAs("name", "age", "state");
26 | StringTokenizer.Tokenize("name\tage state").ShouldHaveTheSameElementsAs("name", "age", "state");
27 | StringTokenizer.Tokenize("name age state").ShouldHaveTheSameElementsAs("name", "age", "state");
28 | }
29 |
30 | [Fact]
31 | public void tokenize_string_with_only_one_token()
32 | {
33 | StringTokenizer.Tokenize("name").ShouldHaveTheSameElementsAs("name");
34 | StringTokenizer.Tokenize(" name ").ShouldHaveTheSameElementsAs("name");
35 | StringTokenizer.Tokenize("\nname").ShouldHaveTheSameElementsAs("name");
36 | StringTokenizer.Tokenize("name\n").ShouldHaveTheSameElementsAs("name");
37 | }
38 |
39 | [Fact]
40 | public void tokenize_string_marked_with_parantheses()
41 | {
42 | StringTokenizer.Tokenize("name \"jeremy miller\" age").ShouldHaveTheSameElementsAs("name", "jeremy miller", "age");
43 | StringTokenizer.Tokenize("name \" jeremy miller\" age").ShouldHaveTheSameElementsAs("name", " jeremy miller", "age");
44 | StringTokenizer.Tokenize("name \"jeremy miller\" age").ShouldHaveTheSameElementsAs("name", "jeremy miller", "age");
45 | StringTokenizer.Tokenize("name \"jeremy miller\" age \"Texas\"").ShouldHaveTheSameElementsAs("name", "jeremy miller", "age", "Texas");
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Tests/Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net7.0;net8.0;net9.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 | all
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Tests/can_use_fields_as_arguments_and_flags.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Oakton;
4 | using Shouldly;
5 | using Xunit;
6 |
7 | namespace Tests
8 | {
9 | public class can_use_fields_as_arguments_and_flags
10 | {
11 | [Fact]
12 | public void integrated_test_arguments_only()
13 | {
14 | var input = build("file1", "red");
15 | input.File.ShouldBe("file1");
16 | input.Color.ShouldBe(Color.red);
17 |
18 | // default is not touched
19 | input.OrderFlag.ShouldBe(0);
20 | }
21 |
22 | [Fact]
23 | public void integrated_test_with_mix_of_flags()
24 | {
25 | var input = build("file1", "--color", "green", "blue", "--order", "12");
26 | input.File.ShouldBe("file1");
27 | input.Color.ShouldBe(Color.blue);
28 | input.ColorFlag.ShouldBe(Color.green);
29 | input.OrderFlag.ShouldBe(12);
30 | }
31 |
32 | [Fact]
33 | public void integrated_test_with_a_boolean_flag()
34 | {
35 | var input = build("file1", "blue", "--true-false");
36 | input.TrueFalseFlag.ShouldBeTrue();
37 |
38 | build("file1", "blue").TrueFalseFlag.ShouldBeFalse();
39 | }
40 |
41 | [Fact]
42 | public void long_flag_with_dashes_should_pass()
43 | {
44 | var input = build("file1", "blue", "--herp-derp");
45 | input.HerpDerpFlag.ShouldBeTrue();
46 |
47 | build("file1", "blue").HerpDerpFlag.ShouldBeFalse();
48 | }
49 |
50 |
51 | private FieldModel build(params string[] tokens)
52 | {
53 | var queue = new Queue(tokens);
54 | var graph = new FieldCommand().Usages;
55 | var creator = new ActivatorCommandCreator();
56 |
57 | return (FieldModel)graph.BuildInput(queue, creator);
58 | }
59 |
60 | public class FieldModel
61 | {
62 | public string File ;
63 | public Color ColorFlag ;
64 |
65 | public Color Color ;
66 | public int OrderFlag ;
67 | public bool TrueFalseFlag ;
68 | [FlagAlias('T')]
69 | public bool TrueOrFalseFlag ;
70 |
71 | public IEnumerable SillyFlag ;
72 |
73 | public bool HerpDerpFlag ;
74 |
75 | [FlagAlias("makesuckmode")]
76 | public bool MakeSuckModeFlag ;
77 |
78 | public IEnumerable Ages ;
79 |
80 | [FlagAlias("aliased", 'a')]
81 | public string AliasedFlag ;
82 | }
83 |
84 | public class FieldCommand : OaktonCommand
85 | {
86 | public FieldCommand()
87 | {
88 | Usage("default").Arguments(x => x.File, x => x.Color);
89 | Usage("ages").Arguments(x => x.File, x => x.Color, x => x.Ages);
90 | }
91 |
92 | public override bool Execute(FieldModel input)
93 | {
94 | throw new NotImplementedException();
95 | }
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/Tests/filtering_launcher_args.cs:
--------------------------------------------------------------------------------
1 | using Oakton.Internal;
2 | using Shouldly;
3 | using Xunit;
4 |
5 | namespace Tests
6 | {
7 | public class filtering_launcher_args
8 | {
9 | [Fact]
10 | public void nothing_for_an_empty_array()
11 | {
12 | new string[0].FilterLauncherArgs()
13 | .Length.ShouldBe(0);
14 | }
15 |
16 | [Fact]
17 | public void just_the_launcher_args()
18 | {
19 | new string[]{"%launcher_args%"}.FilterLauncherArgs()
20 | .Length.ShouldBe(0);
21 | }
22 |
23 | [Fact]
24 | public void extra_args()
25 | {
26 | new string[]{"%launcher_args%", "command"}.FilterLauncherArgs()
27 | .ShouldHaveSingleItem("command");
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Tests/using_injected_services.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Hosting;
5 | using Oakton;
6 | using Oakton.Help;
7 | using Shouldly;
8 | using Xunit;
9 |
10 | [assembly: OaktonCommandAssembly]
11 |
12 | namespace Tests;
13 |
14 | public class using_injected_services
15 | {
16 | [Fact]
17 | public async Task can_use_injected_services()
18 | {
19 | var success = await Host.CreateDefaultBuilder()
20 | .ConfigureServices(services =>
21 | {
22 | services.AddScoped();
23 | services.AddScoped();
24 | })
25 | .RunOaktonCommands(new string[] { "injected", "Bob Marley" });
26 |
27 | success.ShouldBe(0);
28 |
29 | MyService.WasCalled.ShouldBeTrue();
30 | MyService.Name.ShouldBe("Bob Marley");
31 | MyService.WasDisposed.ShouldBeTrue();
32 |
33 | OtherService.WasCalled.ShouldBeTrue();
34 | OtherService.Name.ShouldBe("Bob Marley");
35 | OtherService.WasDisposed.ShouldBeTrue();
36 |
37 | }
38 | }
39 |
40 | public class InjectedInput
41 | {
42 | public string Name { get; set; }
43 | }
44 |
45 | [Description("Injected command", Name = "injected")]
46 | public class InjectedCommand : OaktonCommand
47 | {
48 | [InjectService]
49 | public MyService One { get; set; }
50 |
51 | [InjectService]
52 | public OtherService Two { get; set; }
53 |
54 | public override bool Execute(InjectedInput input)
55 | {
56 | One.DoStuff(input.Name);
57 | Two.DoStuff(input.Name);
58 |
59 | return true;
60 | }
61 | }
62 |
63 | public class MyService : IDisposable
64 | {
65 | public static bool WasCalled;
66 | public static string Name;
67 |
68 | public static bool WasDisposed;
69 |
70 | public void DoStuff(string name)
71 | {
72 | WasCalled = true;
73 | Name = name;
74 | }
75 |
76 | public void Dispose()
77 | {
78 | WasDisposed = true;
79 | }
80 | }
81 |
82 | public class OtherService : IDisposable
83 | {
84 | public static bool WasCalled;
85 | public static string Name;
86 |
87 | public static bool WasDisposed;
88 |
89 | public void DoStuff(string name)
90 | {
91 | WasCalled = true;
92 | Name = name;
93 | }
94 |
95 | public void Dispose()
96 | {
97 | WasDisposed = true;
98 | }
99 | }
100 |
101 | public class MyDbContext{}
102 |
103 | public class MyInput
104 | {
105 |
106 | }
107 |
108 | #region sample_MyDbCommand
109 |
110 | public class MyDbCommand : OaktonAsyncCommand
111 | {
112 | [InjectService]
113 | public MyDbContext DbContext { get; set; }
114 |
115 | public override Task Execute(MyInput input)
116 | {
117 | // do stuff with DbContext from up above
118 | return Task.FromResult(true);
119 | }
120 | }
121 |
122 | #endregion
--------------------------------------------------------------------------------
/src/WorkerService/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using Oakton;
7 |
8 | namespace WorkerService
9 | {
10 | #region sample_using_ihost_activation
11 |
12 | public class Program
13 | {
14 | public static Task Main(string[] args)
15 | {
16 | return CreateHostBuilder(args)
17 | .RunOaktonCommands(args);
18 | }
19 |
20 | public static IHostBuilder CreateHostBuilder(string[] args) =>
21 | // This is a little old-fashioned, but still valid .NET core code:
22 | Host.CreateDefaultBuilder(args)
23 | .ConfigureServices((hostContext, services) => { services.AddHostedService(); });
24 | }
25 |
26 | #endregion
27 | }
--------------------------------------------------------------------------------
/src/WorkerService/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "WorkerService": {
4 | "commandName": "Project",
5 | "dotnetRunMessages": "true",
6 | "environmentVariables": {
7 | "DOTNET_ENVIRONMENT": "Development"
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/WorkerService/Worker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.Hosting;
6 | using Microsoft.Extensions.Logging;
7 |
8 | namespace WorkerService
9 | {
10 | public class Worker : BackgroundService
11 | {
12 | private readonly ILogger _logger;
13 |
14 | public Worker(ILogger logger)
15 | {
16 | _logger = logger;
17 | }
18 |
19 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
20 | {
21 | while (!stoppingToken.IsCancellationRequested)
22 | {
23 | _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
24 | await Task.Delay(1000, stoppingToken);
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/WorkerService/WorkerService.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net7.0
5 | dotnet-WorkerService-88331894-5452-4449-92B7-95E78F129B94
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/WorkerService/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/WorkerService/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/quickstart/quickstart.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net6.0
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------