-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to dynamically create and remove commands? And other features requests #186
Comments
Hi @iskandersierra , thanks for a great and inspiring concept. Some of my first thoughts:
|
... and one more:
|
Awesome response to know where you currently stand on the subject. I'm thinking on the following use case:
My requirements are for cases when the command can only be configured from runtime information. |
In fact, I think you are right to think that Interactive could be moved to it's own nuget, or even better, leave actual interactive mode to cover it's current scope (more build-time oriented), and have a sort of |
Now I see your problem better - but i need to rethink it. Leaving interactive mode to cover it's current scope is a good idea, but I'm not sure |
Some implementation ideas/remarks:
Overall, it seems DynamicCommands is a quite easy concept to implement. |
I created issues for Typin.Modes.Batch #190 and Typin.Modes.Interactive #191, as well as to analyze areas concept #187. Going back to dynamic commands, I would be super happy if you start implementation (you have my full support, and I hope we will end up with a nicely implemented solution - that hopefully will be released in Typin 3.1 ;> ). PS. What I also find out that to support DynamicCommands:
|
If you think this idea fits what I'm describing, we can discuss it at your will, I can also help you if you need me to. However having a single The mutability of the Something like (it might have typos): public interface ICommand
{
CommandMetadata Metadata { get; }
IReadOnlyCollection<CommandArgument> Arguments { get; }
IReadOnlyCollection<CommandOption> Options { get; }
ValueTask ExecuteAsync(
IConsole console,
IReadOnlyCollection<object> arguments,
IReadOnlyDictionary<string, object> options);
}
public record CommandMetadata(
string Keyword,
string Description // more metadata, like explicit examples, ...
); Now you can offer a base class, like the following, to make easier to define commands like today with Typin: public abstract class CommandBase : ICommand
{
// Here implement ICommand explicitly to extract metadata, arguments and options using reflection from this.GetType()
// And provide a SourceGenerator alternative to make it more performant and clean
// Implementation of interface ExecuteAsync would fill the properties by reflection from the given arguments and properties
// and call the simpler ExecuteAsync below
protected abstract ValueTask ExecuteAsync(IConsole console);
} Then a sample command could be implemented like: // Using reflection
[CLICommand("sample", "Description")]
public partial class SampleCommand : CommandBase
{
[CommandArgument(0)]
public int MyArgument { get; set; }
protected override async ValueTask ExecuteAsync(IConsole console) { ... }
}
// Using SourceGenerators
[CLICommand("sample", "Description")]
public partial class SampleCommand
{
[CommandArgument(0)]
public int MyArgument { get; set; }
protected async ValueTask ExecuteAsync(IConsole console) { ... }
} And going beyond, and this is taken from designs of our internal tools, I would like to be able to have a clise like the following: [Command("sample", "Description")]
public class SampleCommands
{
public SampleController(IService service)
{
this.service = service;
}
[SubCommand(Description = "List samples")] // Default route
public ValueTask List(
[CommandOption("name", "n")] string name,
[CommandArgument(0, Description = "...")] FileInfo file,
[ScopedService] IRepository repository,
[ScopedService] IConsole console, // IConsole is just injected like any other service
CancellationToken ct = default // CancellationToken is "injected" from the scope, taken from the IConsole itself
)
{
...
}
[SubCommand("create", Description = "Create sample")] // Default route
public ValueTask Create(...)
{
...
}
} And then a call to I'm sorry if I'm bombarding you with seemingly unconnected ideas :) I you decide to ponder on them I can work with you to design and implement them. |
I don't know whether we can have more providers for "standard" commands or not. Fluent builder/Expression based provider - strange? I'm also concerned about abusing this mechanism by Typin users - maybe this should be an internal mechanism - no user defined ICommandProviders. EDIT:
In my opinion, every command needs to be registered in RootSchema (central repository of all available commands - even if it is mutable)". A simple justification: autocomplete uses it as a source of commands, and also some future command suggestion system may also use it. Schemas are Typin's "reflection" - a window/insight to commands structure.
Probably IDynamicCommand : ICommend inheritance should exist if we go with "Option 1" from point 8.?
(sorry for typos and bad indent - written quickly in github editor) [Command("connect")]
public class ConnectCommand : ICommand
{
private readonly IDynamicCommandReposiotry _service;
public ConnectCommand(IRootSchemaAccessor/IDynamicCommandRepository service)
{
_service = service;
}
public Task ExecuteAsync(IConsole console) //I know Cancellation token should be here - it will be in future -probably with an ability to cancel a single command and not terminate the whole app
{
//Get open api specification - http call
//Build command
//Note #1: command can be build in some user defined service
//Note #2: while building dynamic command definition/schema we will provide a type of IDynamicCommand implementation class that will be a handler for this dynamic command
CommandSchema dynamicCommandSchema = .........; //maybe we need DynamicCommandSchema : CommandSchema?
_service.AddDynamicCommand(dynamicCommandSchema);
}
}
public class ListDynamicCommand : IDynamicCommand
{
//Option 1. init only props for arguments
//Option 2. pass arguments to ExecuteAsync
public Task ExecuteAsync(....) //
}
public class GetPagedDynamicCommand : IDynamicCommand
{
//Option 1. init only props for arguments
//Option 2. pass arguments to ExecuteAsync
public Task ExecuteAsync(....) //
} |
|
I propose I start working on the minimal changes so we can play with the ideas until it is just good enough to have a an iteration. I will fork the repo and start working on a draft PR so we can share ideas in the code. 1- The draft with SubCommands would be just another way (maybe given by an extension package) that would be possible with the ICommandProvider I'm thinking of. The Core would still be ICommand/IDynamicCommand and their providers 2- The idea behind ICommandProvider/IDynamicCommandProvider is to have an extensibility point to produce commands. In your current implemenmtation, RootSchema is the only provider of an immutable collection of commands (added through 3- 4- Yes, 5- In my example, when you run Later I'll keep commenting your issues. I'll have to get back to work :) |
The more I think about these changes, the more I like them. 1,4. We may have NuGet packges:
2. let's leave dynamic directives for some much later iteration - I don't event know if dynamic directives are useful :) " It would ask every time or just annotatate to events in the providers to know if the provider is indicating new commands are available." - I like this, especially events. 5. ok |
Just to take it from where I left before: 6- As explained before, 7- Agreed that we have to rework a bit the interface hierarchy facing the user, to have it easy to use most of the times, and powerful enough for the complex cases 8- That's not what I have in mind, but again, we just need to converge to a common core framework, have it well designed to support the features we are talking about. As said before, I would do it the other way around, The 9- It wouldn't be dynamic class generation. We would have a single 10- As said before, a command provider from methods, would be just a different implementation of I just mentioned dynamic directives as a tangent, that would be a possibility. But I would event think about turning directives into commands (core commands that you could enable or not). I like command
Let me know if this would be compatible with your current approach. Other things I would like too is to be able to provide a custom Prompt, but it could collide with showing current path:
And colored console would also be a plus, and it would be needed into the Maybe this is too much for now, buy I like adding wood to the fire :) |
Don't worry, I also like adding wood to fire ;) |
I'll try to make a first draft of a design (just interfaces and data objects) this weekend to start discussing concrete issues, before starting to do any implementation. It shouldn't take me to communicate my ideas better to you, and it would allow me to know your thoughts about them. Do you think this approach is suitable to you? |
Yes, interfaces are crucial so it a great idea to start from them. |
@iskandersierra Hi, I have started implementing dynamic commands on Some hints where to look first:
|
Is your feature request related to a problem? Please describe.
I would like to be able to add/remove commands dynamically from the app, depending on executions of previous commands (in interactive mode), to achieve something like http-repl tool in
dotnet
.Describe the solution you'd like
I was checking your source code and I think getting the
IRootSchemaAccessor
and fiddling withCommands
property is the way to go, but I just want confirmation from your part, if possible.And once a command is added, how to include some help message, arguments/options descriptions (dynamically generated too)?
How to add multi-line content, like a HTTP POST content?
Describe alternatives you've considered
I havn't tried it yet, just wondering if this library can be a replacement for some internal cli-like tools we have, and this dynamic case is the Non Plus Ultra roadblock we get on every other solution out there.
Truth be told, I would prefer some kind of
ICommandsProvider
returning a collection of provided ICommands, and the RootSchema or the like, would Get anIEnumerable<ICommandsProvider>
to obtain a composite collection of all the available commands. Some breaking changes might be required to achieve that, but it would add an extra layer of extensibility.With this issue I just want to open a discussion around making this library more extensible. I'm willing to work on some features once well defined, if you agree.
The text was updated successfully, but these errors were encountered: