Initial commit

This commit is contained in:
Fergal Moran
2024-07-26 17:01:40 +01:00
commit 2daa52aa04
14 changed files with 362 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Created by https://www.toptal.com/developers/gitignore/api/dotnetcore
# Edit at https://www.toptal.com/developers/gitignore?templates=dotnetcore
### DotnetCore ###
# .NET Core build folders
bin/
obj/
# Common node modules locations
/node_modules
/wwwroot/node_modules
# End of https://www.toptal.com/developers/gitignore/api/dotnetcore
.idea/
.krud/

View File

@@ -0,0 +1,34 @@
using Microsoft.Extensions.Options;
using Snapp.Cli.Helpers;
using Spectre.Console;
using Spectre.Console.Cli;
namespace Snapp.Cli.Commands;
public class AddSnappsCommand : Command<AddSnappsCommand.Settings> {
public sealed class Settings : DefaultCommandSettings {
public Settings(IOptions<AppSettings> settings) : base(settings) { }
}
public override int Execute(CommandContext context, Settings settings) {
if (string.IsNullOrEmpty(settings.ServerUrl)) {
settings.ServerUrl = AnsiConsole.Prompt(
new TextPrompt<string>("Snapp server address?")
.PromptStyle("green"));
}
if (string.IsNullOrEmpty(settings.ApiKey)) {
settings.ApiKey = AnsiConsole.Prompt(
new TextPrompt<string>("Snapp server API Key?")
.PromptStyle("green"));
}
Console.WriteLine(
$"Executing list snapps command with server address: {settings.ServerUrl} and API key: {settings.ApiKey}");
// var snaps = await service.GetSnaps();
// foreach (var snap in snaps.Results) {
// Console.WriteLine($"{snap.Id}: {snap.Title}");
// }
return -1;
}
}

17
Commands/DebugCommand.cs Normal file
View File

@@ -0,0 +1,17 @@
using System.ComponentModel;
using Spectre.Console.Cli;
namespace Snapp.Cli.Commands;
public class DebugCommand : AsyncCommand<DebugCommand.Settings> {
public class Settings : CommandSettings {
[CommandArgument(1, "<ECHO-TEXT>")]
[Description("Gimme some text to echo.")]
public string? EchoText { get; set; }
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings) {
Console.WriteLine($"Echoing: {settings.EchoText}");
return await Task.FromResult(3);
}
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel;
using Microsoft.Extensions.Options;
using Snapp.Cli.Helpers;
using Spectre.Console;
using Spectre.Console.Cli;
namespace Snapp.Cli.Commands;
public class DefaultCommandSettings : CommandSettings {
public readonly AppSettings Config;
public DefaultCommandSettings(IOptions<AppSettings> settings) {
Config = settings.Value;
}
[Description("Path to search. Defaults to current directory.")]
[CommandArgument(0, "[-s|--server]")]
public string? ServerUrl { get; set; }
[Description("API Key for server.")]
[CommandArgument(0, "[-k|--api-key]")]
public string? ApiKey { get; set; }
public static string AskServerIfMissing(string? current) =>
!string.IsNullOrWhiteSpace(current)
? current
: AnsiConsole.Prompt(
new TextPrompt<string>("Enter your server URL:")
.PromptStyle("green")
.Validate(serverAddress => !string.IsNullOrWhiteSpace(serverAddress)
? ValidationResult.Success()
: ValidationResult.Error("Server URL cannot be empty.")));
public static string AskApiKeyIfMissing(string? current) =>
!string.IsNullOrWhiteSpace(current)
? current
: AnsiConsole.Prompt(
new TextPrompt<string>("Enter your API Key:")
.PromptStyle("green")
.Validate(serverAddress => !string.IsNullOrWhiteSpace(serverAddress)
? ValidationResult.Success()
: ValidationResult.Error("API Key cannot be empty.")));
}

View File

@@ -0,0 +1,24 @@
using Microsoft.Extensions.Options;
using Snapp.Cli.Helpers;
using Snapp.Cli.Services;
using Spectre.Console.Cli;
namespace Snapp.Cli.Commands;
public class ListSnappsCommand : AsyncCommand<ListSnappsCommand.Settings> {
private readonly SnappService _snappService;
public ListSnappsCommand(SnappService snappService) {
_snappService = snappService;
}
public sealed class Settings : CommandSettings { }
// public sealed class Settings : CommandSettings {
// public Settings(IOptions<AppSettings> settings) : base(settings) { }
// }
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings) {
Console.WriteLine($"Listing shit");
return 1;
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.Extensions.Configuration;
namespace Snapp.Cli.Helpers;
public class AppSettings {
public string? ApiKey { get; set; }
public string? ServerUrl { get; set; }
}
public class AppSettingsHelper {
private readonly AppSettings _config;
public string? ApiKey { get => _config.ApiKey; }
public string? ServerUrl { get => _config.ServerUrl; }
private static string SettingsFile {
get => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"snapp-cli/config.json");
}
public AppSettingsHelper() {
_config = _initialiseSettings();
}
private AppSettings _initialiseSettings() {
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile(SettingsFile, false, true)
.Build();
if (config is null) {
throw new NullReferenceException("Missing settings file");
}
var c = config.Get<AppSettings>();
if (c is null) {
throw new NullReferenceException("Missing settings");
}
return c;
}
}

32
Helpers/TypeRegistrar.cs Normal file
View File

@@ -0,0 +1,32 @@
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;
namespace Snapp.Cli.Helpers;
public sealed class TypeRegistrar : ITypeRegistrar {
private readonly IServiceCollection _builder;
public TypeRegistrar(IServiceCollection builder) {
_builder = builder;
}
public ITypeResolver Build() {
return new TypeResolver(_builder.BuildServiceProvider());
}
public void Register(Type service, Type implementation) {
_builder.AddSingleton(service, implementation);
}
public void RegisterInstance(Type service, object implementation) {
_builder.AddSingleton(service, implementation);
}
public void RegisterLazy(Type service, Func<object> func) {
if (func is null) {
throw new ArgumentNullException(nameof(func));
}
_builder.AddSingleton(service, (provider) => func());
}
}

19
Helpers/TypeResolver.cs Normal file
View File

@@ -0,0 +1,19 @@
using Spectre.Console.Cli;
namespace Snapp.Cli.Helpers;
public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver, IDisposable {
private readonly IServiceProvider _provider =
provider ??
throw new ArgumentNullException(nameof(provider));
public object? Resolve(Type? type) {
return type == null ? null : _provider.GetService(type);
}
public void Dispose() {
if (_provider is IDisposable disposable) {
disposable.Dispose();
}
}
}

29
Program.cs Normal file
View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Snapp.Cli.Commands;
using Snapp.Cli.Helpers;
using Snapp.Cli.Services;
using Spectre.Console.Cli;
var registrations = new ServiceCollection();
registrations.AddSingleton<SnappService>();
registrations.AddSingleton<AppSettingsHelper>();
registrations.AddHttpClient<ISnappService, SnappService>();
var registrar = new Snapp.Cli.Helpers.TypeRegistrar(registrations);
var app = new CommandApp<DebugCommand>(registrar);
app.Configure(config => {
#if DEBUG
config.PropagateExceptions();
config.ValidateExamples();
#endif
config.AddCommand<DebugCommand>("debug");
config.AddCommand<ListSnappsCommand>("list");
});
return await app
.RunAsync(args)
.ConfigureAwait(false);

View File

@@ -0,0 +1,14 @@
{
"Servers": [
{
"Default": {
"ApiKey": "<YOUR_API_KEY>",
"ServerUrl": "<YOUR_SNAPP_SERVER_URL>"
},
"Other": {
"ApiKey": "<YOUR_OTHER_API_KEY>",
"ServerUrl": "<YOUR_OTHER_SNAPP_SERVER_URL>"
}
}
]
}

37
Services/SnappService.cs Normal file
View File

@@ -0,0 +1,37 @@
using System.Text.Json;
using Snapp.Cli.Helpers;
namespace Snapp.Cli.Services;
public class SnappApiResponse<T> {
public int Count;
public int Page;
public int Limit;
public string? SortDirection;
public T? Results { get; set; }
}
public record Snapp(string Id, string Title, string Description);
public interface ISnappService {
public Task<SnappApiResponse<List<Snapp>>?> GetSnaps();
}
public class SnappService : ISnappService {
private readonly HttpClient _httpClient;
private readonly AppSettingsHelper _settingsHelper;
public SnappService(HttpClient httpClient, AppSettingsHelper settingsHelper) {
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {settingsHelper.ApiKey}");
_settingsHelper = settingsHelper;
}
public async Task<SnappApiResponse<List<Snapp>>?> GetSnaps() {
var url = Flurl.Url.Combine(_settingsHelper.ServerUrl, "/snapps");
var response = await _httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<SnappApiResponse<List<Snapp>>>(content);
}
}

24
snapp-cli.csproj Normal file
View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Snapp.Cli</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cocona" Version="2.2.0"/>
<PackageReference Include="Flurl" Version="4.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/>
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\config.json" />
<EmbeddedResource Include="Resources\defaultconfig.json" />
</ItemGroup>
</Project>

25
snapp-cli.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "snapp-cli", "snapp-cli.csproj", "{4016DB59-BCF0-4C0B-8229-AD686999EBDE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4016DB59-BCF0-4C0B-8229-AD686999EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4016DB59-BCF0-4C0B-8229-AD686999EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4016DB59-BCF0-4C0B-8229-AD686999EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4016DB59-BCF0-4C0B-8229-AD686999EBDE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {67F03902-702A-4C51-A443-739B736E2A95}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfigurationBinder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Ffergalm_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F5890bea1acfbf3fed68846cf88de417726aefd9611192c0103fe2da89fa41_003FConfigurationBinder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Ffergalm_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbf9021a960b74107a7e141aa06bc9d8a0a53c929178c2fb95b1597be8af8dc_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIConfigurator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Ffergalm_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb292bc073ef4ad0e85311a67c8972e3da13f217b81775ed3eb2fa38e4fa2d32_003FIConfigurator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATypeRegistrar_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Ffergalm_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fef4e8f922fc4e26f8bd814dcaba442da850104c6523e517b0ac47e83c7e2da_003FTypeRegistrar_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>