mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-31 01:40:42 +00:00
Esiur CLI
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
namespace Esiur.CLI.Configuration;
|
||||
|
||||
public sealed class CliConfiguration
|
||||
{
|
||||
public string? DefaultProfile { get; set; }
|
||||
public string OutputFormat { get; set; } = "table";
|
||||
public string Timeout { get; set; } = "30s";
|
||||
public Dictionary<string, ConnectionProfile> Profiles { get; set; }
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace Esiur.CLI.Configuration;
|
||||
|
||||
public sealed record GlobalOptions(
|
||||
string? Profile,
|
||||
string? Endpoint,
|
||||
string? Provider,
|
||||
string? Identity,
|
||||
string? Output,
|
||||
TimeSpan? Timeout,
|
||||
bool Verbose,
|
||||
bool Debug);
|
||||
|
||||
public sealed record ResolvedConnection(
|
||||
string DisplayName,
|
||||
string Endpoint,
|
||||
string? Provider,
|
||||
string? Identity,
|
||||
string? Domain,
|
||||
string OutputFormat,
|
||||
TimeSpan Timeout,
|
||||
ConnectionProfile? Profile);
|
||||
|
||||
public static class ConfigurationResolver
|
||||
{
|
||||
public static ResolvedConnection Resolve(CliConfiguration configuration, GlobalOptions options)
|
||||
{
|
||||
var profileName = options.Profile
|
||||
?? Environment.GetEnvironmentVariable("ESIUR_PROFILE")
|
||||
?? configuration.DefaultProfile;
|
||||
ConnectionProfile? profile = null;
|
||||
if (!string.IsNullOrWhiteSpace(profileName)
|
||||
&& !configuration.Profiles.TryGetValue(profileName, out profile))
|
||||
throw new CliException($"Profile \"{profileName}\" was not found.", ExitCodes.InvalidArguments);
|
||||
|
||||
var endpoint = options.Endpoint
|
||||
?? Environment.GetEnvironmentVariable("ESIUR_ENDPOINT")
|
||||
?? profile?.Endpoint;
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
throw new CliException("No endpoint was supplied and no profile is selected.", ExitCodes.InvalidArguments);
|
||||
|
||||
ValidateEndpoint(endpoint);
|
||||
var output = options.Output
|
||||
?? Environment.GetEnvironmentVariable("ESIUR_OUTPUT")
|
||||
?? profile?.OutputFormat
|
||||
?? configuration.OutputFormat;
|
||||
OutputFormatExtensions.Parse(output);
|
||||
|
||||
var timeout = options.Timeout
|
||||
?? DurationParser.TryParse(Environment.GetEnvironmentVariable("ESIUR_TIMEOUT"))
|
||||
?? DurationParser.Parse(configuration.Timeout);
|
||||
|
||||
return new ResolvedConnection(
|
||||
profile?.Name ?? "endpoint",
|
||||
endpoint,
|
||||
options.Provider ?? Environment.GetEnvironmentVariable("ESIUR_PROVIDER") ?? profile?.Provider,
|
||||
options.Identity ?? Environment.GetEnvironmentVariable("ESIUR_IDENTITY") ?? profile?.Identity,
|
||||
profile?.Domain,
|
||||
output,
|
||||
timeout,
|
||||
profile);
|
||||
}
|
||||
|
||||
public static void ValidateEndpoint(string endpoint)
|
||||
{
|
||||
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)
|
||||
|| !string.Equals(uri.Scheme, "ep", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.IsNullOrWhiteSpace(uri.Host)
|
||||
|| !string.IsNullOrEmpty(uri.UserInfo))
|
||||
throw new CliException($"Endpoint \"{endpoint}\" is not a valid ep:// endpoint.", ExitCodes.InvalidArguments);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DurationParser
|
||||
{
|
||||
public static TimeSpan Parse(string value) => TryParse(value)
|
||||
?? throw new CliException($"Duration \"{value}\" is invalid. Use values such as 500ms, 30s, 5m, or 1h.", ExitCodes.InvalidArguments);
|
||||
|
||||
public static TimeSpan? TryParse(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
value = value.Trim();
|
||||
var suffixes = new (string Suffix, double Factor)[]
|
||||
{
|
||||
("ms", 1), ("s", 1_000), ("m", 60_000), ("h", 3_600_000),
|
||||
};
|
||||
foreach (var item in suffixes)
|
||||
if (value.EndsWith(item.Suffix, StringComparison.OrdinalIgnoreCase)
|
||||
&& double.TryParse(value[..^item.Suffix.Length],
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var number)
|
||||
&& number >= 0)
|
||||
return TimeSpan.FromMilliseconds(number * item.Factor);
|
||||
return TimeSpan.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, out var parsed)
|
||||
&& parsed >= TimeSpan.Zero ? parsed : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Esiur.CLI.Configuration;
|
||||
|
||||
public interface IConfigurationStore
|
||||
{
|
||||
Task<CliConfiguration> LoadAsync(CancellationToken cancellationToken);
|
||||
Task SaveAsync(CliConfiguration configuration, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class ConfigurationStore : IConfigurationStore
|
||||
{
|
||||
static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
public ConfigurationStore(string? path = null) => Path = path ?? GetDefaultPath();
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public static string GetDefaultPath()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
return System.IO.Path.Combine(appData, "Esiur", "config.json");
|
||||
}
|
||||
|
||||
var configHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
if (string.IsNullOrWhiteSpace(configHome))
|
||||
configHome = System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
|
||||
return System.IO.Path.Combine(configHome, "esiur", "config.json");
|
||||
}
|
||||
|
||||
public async Task<CliConfiguration> LoadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(Path))
|
||||
return new CliConfiguration();
|
||||
|
||||
await using var stream = File.OpenRead(Path);
|
||||
var value = await JsonSerializer.DeserializeAsync<CliConfiguration>(stream, JsonOptions, cancellationToken)
|
||||
?? new CliConfiguration();
|
||||
value.Profiles = new Dictionary<string, ConnectionProfile>(
|
||||
value.Profiles ?? [], StringComparer.OrdinalIgnoreCase);
|
||||
return value;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(CliConfiguration configuration, CancellationToken cancellationToken)
|
||||
{
|
||||
var directory = System.IO.Path.GetDirectoryName(Path)!;
|
||||
Directory.CreateDirectory(directory);
|
||||
var temporary = Path + ".tmp";
|
||||
await using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
await JsonSerializer.SerializeAsync(stream, configuration, JsonOptions, cancellationToken);
|
||||
File.Move(temporary, Path, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Esiur.CLI.Configuration;
|
||||
|
||||
public sealed class ConnectionProfile
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string Endpoint { get; init; }
|
||||
public string? Provider { get; init; }
|
||||
public string? Identity { get; init; }
|
||||
public string? Domain { get; init; }
|
||||
public string? DefaultResourcePath { get; init; }
|
||||
public string OutputFormat { get; init; } = "table";
|
||||
}
|
||||
Reference in New Issue
Block a user