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,251 @@
|
||||
using Esiur.CLI.Rendering;
|
||||
using Esiur.Data.Types;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
|
||||
namespace Esiur.CLI.Client;
|
||||
|
||||
public sealed record ResourceSummary(
|
||||
string Name,
|
||||
string Path,
|
||||
uint SessionId,
|
||||
string Type,
|
||||
string TypeId,
|
||||
int ChildCount);
|
||||
|
||||
public sealed record PropertyDescription(
|
||||
string Name,
|
||||
byte Index,
|
||||
string Type,
|
||||
bool ReadOnly,
|
||||
bool Constant,
|
||||
bool Historical,
|
||||
bool Inherited,
|
||||
object? Value,
|
||||
IReadOnlyDictionary<string, string> Annotations);
|
||||
|
||||
public sealed record FunctionDescription(
|
||||
string Name,
|
||||
byte Index,
|
||||
string Arguments,
|
||||
string ReturnType,
|
||||
string Flags,
|
||||
bool Inherited,
|
||||
IReadOnlyDictionary<string, string> Annotations);
|
||||
|
||||
public sealed record EventDescription(
|
||||
string Name,
|
||||
byte Index,
|
||||
string ArgumentType,
|
||||
bool Subscribable,
|
||||
bool Inherited,
|
||||
IReadOnlyDictionary<string, string> Annotations);
|
||||
|
||||
public sealed record ConstantDescription(
|
||||
string Name,
|
||||
byte Index,
|
||||
string Type,
|
||||
object? Value,
|
||||
bool Inherited,
|
||||
IReadOnlyDictionary<string, string> Annotations);
|
||||
|
||||
public sealed record ResourceDescription(
|
||||
string Path,
|
||||
string Type,
|
||||
string TypeId,
|
||||
uint SessionId,
|
||||
ulong Age,
|
||||
string? ParentTypeId,
|
||||
IReadOnlyDictionary<string, string> Annotations,
|
||||
IReadOnlyList<PropertyDescription> Properties,
|
||||
IReadOnlyList<FunctionDescription> Functions,
|
||||
IReadOnlyList<EventDescription> Events,
|
||||
IReadOnlyList<ConstantDescription> Constants);
|
||||
|
||||
public sealed record PropertyResult(string Resource, string Property, byte Index, object? Value);
|
||||
|
||||
public sealed class ResourceInspectionService
|
||||
{
|
||||
public async Task<IReadOnlyList<ResourceSummary>> QueryAsync(
|
||||
EsiurSession session,
|
||||
string path,
|
||||
int depth,
|
||||
string? typeFilter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (depth < 1) throw new CliException("Query depth must be at least 1.", ExitCodes.InvalidArguments);
|
||||
var output = new List<ResourceSummary>();
|
||||
var queue = new Queue<(string Path, int Level)>();
|
||||
var seen = new HashSet<uint>();
|
||||
queue.Enqueue((NormalizePath(path), 1));
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var current = queue.Dequeue();
|
||||
IResource[] children;
|
||||
try
|
||||
{
|
||||
children = await EsiurSessionFactory.AwaitReply(
|
||||
session.Connection.Query(current.Path), cancellationToken) ?? [];
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw MapResourceException(current.Path, exception);
|
||||
}
|
||||
|
||||
foreach (var child in children.OfType<EpResource>())
|
||||
{
|
||||
if (!seen.Add(child.ResourceInstanceId)) continue;
|
||||
var childPath = NormalizePath(child.ResourceLink ?? child.Instance.Link);
|
||||
var definition = child.Instance.Definition;
|
||||
var childCount = 0;
|
||||
try
|
||||
{
|
||||
var descendants = await EsiurSessionFactory.AwaitReply(
|
||||
session.Connection.Query(childPath), cancellationToken);
|
||||
childCount = descendants?.Length ?? 0;
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(typeFilter)
|
||||
|| definition.Name.Contains(typeFilter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
output.Add(new ResourceSummary(
|
||||
child.Instance.Name,
|
||||
childPath,
|
||||
child.ResourceInstanceId,
|
||||
definition.Name,
|
||||
TypeIdFormatter.Format(definition.Id),
|
||||
childCount));
|
||||
}
|
||||
|
||||
if (current.Level < depth)
|
||||
queue.Enqueue((childPath, current.Level + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async Task<ResourceDescription> DescribeAsync(
|
||||
EsiurSession session,
|
||||
string path,
|
||||
bool includeValues,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resource = await ResolveAsync(session, path, cancellationToken);
|
||||
var definition = resource.Instance.Definition;
|
||||
return new ResourceDescription(
|
||||
NormalizePath(resource.ResourceLink ?? path),
|
||||
definition.Name,
|
||||
TypeIdFormatter.Format(definition.Id),
|
||||
resource.ResourceInstanceId,
|
||||
resource.Instance.Age,
|
||||
definition.ParentTypeId is ulong parent ? TypeIdFormatter.Format(parent) : null,
|
||||
ToDictionary(definition.Annotations),
|
||||
definition.Properties.OrderBy(x => x.Index).Select(property =>
|
||||
new PropertyDescription(
|
||||
property.Name,
|
||||
property.Index,
|
||||
TypeFormatter.Format(property.ValueType),
|
||||
property.ReadOnly,
|
||||
property.Constant,
|
||||
property.Historical,
|
||||
property.Inherited,
|
||||
includeValues && resource.TryGetPropertyValue(property.Index, out var value) ? value : null,
|
||||
ToDictionary(property.Annotations))).ToArray(),
|
||||
definition.Functions.OrderBy(x => x.Index).Select(function =>
|
||||
new FunctionDescription(
|
||||
function.Name,
|
||||
function.Index,
|
||||
string.Join(", ", (function.Arguments ?? []).Select(argument =>
|
||||
$"{argument.Name}: {TypeFormatter.Format(argument.Type)}{(argument.Optional ? " = optional" : "")}")),
|
||||
TypeFormatter.Format(function.ReturnType),
|
||||
FunctionFlags(function),
|
||||
function.Inherited,
|
||||
ToDictionary(function.Annotations))).ToArray(),
|
||||
definition.Events.OrderBy(x => x.Index).Select(@event =>
|
||||
new EventDescription(
|
||||
@event.Name,
|
||||
@event.Index,
|
||||
TypeFormatter.Format(@event.ArgumentType),
|
||||
@event.Subscribable,
|
||||
@event.Inherited,
|
||||
ToDictionary(@event.Annotations))).ToArray(),
|
||||
definition.Constants.OrderBy(x => x.Index).Select(constant =>
|
||||
new ConstantDescription(
|
||||
constant.Name,
|
||||
constant.Index,
|
||||
TypeFormatter.Format(constant.ValueType),
|
||||
constant.Value,
|
||||
constant.Inherited,
|
||||
ToDictionary(constant.Annotations))).ToArray());
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PropertyResult>> GetAsync(
|
||||
EsiurSession session,
|
||||
string path,
|
||||
IReadOnlyList<string> members,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resource = await ResolveAsync(session, path, cancellationToken);
|
||||
var results = new List<PropertyResult>();
|
||||
foreach (var member in members)
|
||||
{
|
||||
var property = ResolveProperty(resource.Instance.Definition, member)
|
||||
?? throw new CliException(
|
||||
$"Property \"{member}\" was not found on \"{path}\".", ExitCodes.MemberNotFound);
|
||||
if (!resource.TryGetPropertyValue(property.Index, out var value))
|
||||
throw new CliException(
|
||||
$"Property \"{property.Name}\" has no attached value.", ExitCodes.GeneralFailure);
|
||||
results.Add(new PropertyResult(NormalizePath(resource.ResourceLink ?? path), property.Name, property.Index, value));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<EpResource> ResolveAsync(
|
||||
EsiurSession session, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resource = await EsiurSessionFactory.AwaitReply(
|
||||
session.Connection.Get(NormalizePath(path)), cancellationToken);
|
||||
return resource as EpResource
|
||||
?? throw new CliException($"Resource \"{path}\" was not found.", ExitCodes.ResourceNotFound);
|
||||
}
|
||||
catch (CliException) { throw; }
|
||||
catch (Exception exception) { throw MapResourceException(path, exception); }
|
||||
}
|
||||
|
||||
static PropertyDef? ResolveProperty(TypeDef definition, string member)
|
||||
{
|
||||
if (byte.TryParse(member, out var index)) return definition.GetPropertyDefByIndex(index);
|
||||
return definition.GetPropertyDefByName(member);
|
||||
}
|
||||
|
||||
static CliException MapResourceException(string path, Exception exception)
|
||||
{
|
||||
if (exception.Message.Contains("not found", StringComparison.OrdinalIgnoreCase))
|
||||
return new CliException($"Resource \"{path}\" was not found.", ExitCodes.ResourceNotFound, exception);
|
||||
if (exception.Message.Contains("allow", StringComparison.OrdinalIgnoreCase)
|
||||
|| exception.Message.Contains("permission", StringComparison.OrdinalIgnoreCase))
|
||||
return new CliException($"Access to resource \"{path}\" was denied.", ExitCodes.AccessDenied, exception);
|
||||
return new CliException($"Could not inspect resource \"{path}\": {exception.Message}", ExitCodes.GeneralFailure, exception);
|
||||
}
|
||||
|
||||
public static string NormalizePath(string path) => path.Trim().Trim('/');
|
||||
|
||||
static IReadOnlyDictionary<string, string> ToDictionary(Esiur.Data.Map<string, string>? map) =>
|
||||
map is null ? new Dictionary<string, string>() : map.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
static string FunctionFlags(FunctionDef function) => string.Join(", ", new[]
|
||||
{
|
||||
function.IsStatic ? "static" : null,
|
||||
function.ReadOnly ? "read-only" : null,
|
||||
function.Idempotent ? "idempotent" : null,
|
||||
function.Cancellable ? "cancellable" : null,
|
||||
function.StreamMode != StreamMode.None ? $"stream:{function.StreamMode}" : null,
|
||||
function.Pausable ? "pausable" : null,
|
||||
}.Where(x => x is not null));
|
||||
}
|
||||
Reference in New Issue
Block a user