From 09af15b6121230c8d6fa60a2cad82386dd7038cb Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Thu, 27 Feb 2020 21:58:42 -0500 Subject: [PATCH 1/9] simplified NuPlugin api --- samples/Nu.Plugin.Len/Program.cs | 9 +- .../Configuration/PluginConfiguration.cs | 60 ---------- src/Nu.Plugin/Interfaces/INuPluginBuilder.cs | 17 --- src/Nu.Plugin/NuPlugin.cs | 108 +++++++++++------- 4 files changed, 71 insertions(+), 123 deletions(-) delete mode 100644 src/Nu.Plugin/Configuration/PluginConfiguration.cs delete mode 100644 src/Nu.Plugin/Interfaces/INuPluginBuilder.cs diff --git a/samples/Nu.Plugin.Len/Program.cs b/samples/Nu.Plugin.Len/Program.cs index ef899c1..8945464 100644 --- a/samples/Nu.Plugin.Len/Program.cs +++ b/samples/Nu.Plugin.Len/Program.cs @@ -7,11 +7,10 @@ namespace Nu.Plugin.Len { class Program : INuPluginFilter { - static async Task Main(string[] args) => await NuPlugin.Create() - .Name("len") - .Usage("Return the length of a string") - .IsFilter() - .RunAsync(); + static async Task Main() => await NuPlugin + .Build("len") + .Description("Return the length of a string") + .FilterPluginAsync(); public object BeginFilter() => Array.Empty(); diff --git a/src/Nu.Plugin/Configuration/PluginConfiguration.cs b/src/Nu.Plugin/Configuration/PluginConfiguration.cs deleted file mode 100644 index e0d1114..0000000 --- a/src/Nu.Plugin/Configuration/PluginConfiguration.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace Nu.Plugin -{ - internal class PluginConfiguration - { - private PluginConfiguration() { } - - private PluginConfiguration(string name, string usage, bool isFilter, int[] positional, object named) - { - IsFilter = isFilter; - Name = name; - Usage = usage; - Named = named; - Positional = positional; - } - - public static PluginConfiguration Create() => new PluginConfiguration(); - - [JsonPropertyName("name")] - public string Name { get; } - - [JsonPropertyName("usage")] - public string Usage { get; } - - [JsonPropertyName("is_filter")] - public bool IsFilter { get; } = true; - - [JsonPropertyName("positional")] - public int[] Positional { get; } = Array.Empty(); - - [JsonPropertyName("named")] - public object Named { get; } = new { }; - - public PluginConfiguration WithName(string name) => new PluginConfiguration( - name, - this.Usage, - this.IsFilter, - this.Positional, - this.Named - ); - - public PluginConfiguration WithUsage(string usage) => new PluginConfiguration( - this.Name, - usage, - this.IsFilter, - this.Positional, - this.Named - ); - - public PluginConfiguration WithIsFilter(bool isFilter) => new PluginConfiguration( - this.Name, - this.Usage, - isFilter, - this.Positional, - this.Named - ); - } -} \ No newline at end of file diff --git a/src/Nu.Plugin/Interfaces/INuPluginBuilder.cs b/src/Nu.Plugin/Interfaces/INuPluginBuilder.cs deleted file mode 100644 index c9a99c3..0000000 --- a/src/Nu.Plugin/Interfaces/INuPluginBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; -using Nu.Plugin.Interfaces; - -namespace Nu.Plugin -{ - public interface INuPluginBuilder - { - INuPluginBuilder Name(string name); - - INuPluginBuilder Usage(string usage); - - INuPluginBuilder IsFilter() where T: INuPluginFilter, new(); - - Task RunAsync(); - } -} diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index e259920..f66f382 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading.Tasks; @@ -6,25 +7,45 @@ namespace Nu.Plugin { - public class NuPlugin : INuPluginBuilder + public class NuPlugin { private readonly Stream _stdin; private readonly Stream _stdout; + private Signature _signature = Signature.Create(); private StreamWriter _standardOutputWriter; - private PluginConfiguration _configuration = PluginConfiguration.Create(); - - private INuPluginFilter _filter = null; - private NuPlugin(Stream stdin, Stream stdout) { _stdout = stdout; _stdin = stdin; } - public async Task RunAsync() + public static NuPlugin Build(string name = null) { + var stdin = Console.OpenStandardInput(); + var stdout = Console.OpenStandardOutput(); + + return new NuPlugin(stdin, stdout).Name(name); + } + + public NuPlugin Name(string name) + { + _signature = _signature.WithName(name); + return this; + } + + public NuPlugin Description(string description) + { + _signature = _signature.WithUsage(description); + return this; + } + + public async Task SinkPluginAsync() where T : INuPluginSink, new() + { + _signature = _signature.WithIsFilter(true); + var sink = new T(); + using (var standardInput = new StreamReader(_stdin, Console.InputEncoding)) using (_standardOutputWriter = new StreamWriter(_stdout, Console.OutputEncoding)) { @@ -38,22 +59,57 @@ public async Task RunAsync() if (request.Method == "config") { - OkResponse(_configuration); + OkResponse(_signature); break; } - else if (_configuration.IsFilter && request.Method == "begin_filter") + else if (request.Method == "sink") { - OkResponse(_filter.BeginFilter()); + var requestParams = request.GetParams>(); + sink.Sink(requestParams); + break; + } + else + { + break; + } + } + } + } + + public async Task FilterPluginAsync() where T : INuPluginFilter, new() + { + _signature = _signature.WithIsFilter(true); + var filter = new T(); + + using (var standardInput = new StreamReader(_stdin, Console.InputEncoding)) + using (_standardOutputWriter = new StreamWriter(_stdout, Console.OutputEncoding)) + { + _standardOutputWriter.AutoFlush = true; + + while (true) + { + var request = await standardInput.GetNextRequestAsync(); + + if (request is null || !request.IsValid) { break; } + + if (request.Method == "config") + { + OkResponse(_signature); + break; + } + else if (_signature.IsFilter && request.Method == "begin_filter") + { + OkResponse(filter.BeginFilter()); } else if (request.Method == "filter") { var requestParams = request.GetParams(); - RpcValueResponse(_filter.Filter(requestParams)); + RpcValueResponse(filter.Filter(requestParams)); } else if (request.Method == "end_filter") { - OkResponse(_filter.EndFilter()); + OkResponse(filter.EndFilter()); break; } else @@ -74,35 +130,5 @@ private void Response(JsonRpcResponse response) _standardOutputWriter.WriteLine(serializedResponse); } - - public static INuPluginBuilder Create(Stream stdin, Stream stdout) => new NuPlugin(stdin, stdout); - - public static INuPluginBuilder Create() - { - var stdin = Console.OpenStandardInput(); - var stdout = Console.OpenStandardOutput(); - - return new NuPlugin(stdin, stdout); - } - - public INuPluginBuilder Name(string name) - { - _configuration = _configuration.WithName(name); - return this; - } - - public INuPluginBuilder Usage(string usage) - { - _configuration = _configuration.WithUsage(usage); - return this; - } - - public INuPluginBuilder IsFilter() where T : INuPluginFilter, new() - { - _configuration = _configuration.WithIsFilter(true); - _filter = new T(); - - return this; - } } } From 70a198b5b813f25096d3ee9e446298c1a694df3d Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Thu, 27 Feb 2020 22:47:25 -0500 Subject: [PATCH 2/9] add Signature --- src/Nu.Plugin/Interfaces/INuPluginSink.cs | 9 +++ src/Nu.Plugin/NuPlugin.cs | 2 +- src/Nu.Plugin/Signature.cs | 69 +++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/Nu.Plugin/Interfaces/INuPluginSink.cs create mode 100644 src/Nu.Plugin/Signature.cs diff --git a/src/Nu.Plugin/Interfaces/INuPluginSink.cs b/src/Nu.Plugin/Interfaces/INuPluginSink.cs new file mode 100644 index 0000000..8603b22 --- /dev/null +++ b/src/Nu.Plugin/Interfaces/INuPluginSink.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Nu.Plugin.Interfaces +{ + public interface INuPluginSink + { + void Sink(IEnumerable requestParams); + } +} \ No newline at end of file diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index f66f382..ed2ab1b 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -37,7 +37,7 @@ public NuPlugin Name(string name) public NuPlugin Description(string description) { - _signature = _signature.WithUsage(description); + _signature = _signature.WithDescription(description); return this; } diff --git a/src/Nu.Plugin/Signature.cs b/src/Nu.Plugin/Signature.cs new file mode 100644 index 0000000..c2993c7 --- /dev/null +++ b/src/Nu.Plugin/Signature.cs @@ -0,0 +1,69 @@ +using System; +using System.Text.Json.Serialization; + +namespace Nu.Plugin +{ + internal class Signature + { + private Signature() { } + + private Signature(string name, string usage, bool isFilter, int[] positional, object named) + { + IsFilter = isFilter; + Name = name; + Description = usage; + Named = named; + Positional = positional; + } + + public static Signature Create() => new Signature(); + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("usage")] + public string Description { get; } + + [JsonPropertyName("positional")] + public int[] Positional { get; } = Array.Empty(); + + [JsonPropertyName("rest_positional")] + public int[] RestPositional { get; } = Array.Empty(); + + [JsonPropertyName("named")] + public object Named { get; } = new { }; + + [JsonPropertyName("yields")] + public object Yields { get; } = new { }; + + [JsonPropertyName("input")] + public object Input { get; } = new { }; + + [JsonPropertyName("is_filter")] + public bool IsFilter { get; } = false; + + public Signature WithName(string name) => new Signature( + name, + this.Description, + this.IsFilter, + this.Positional, + this.Named + ); + + public Signature WithDescription(string description) => new Signature( + this.Name, + description, + this.IsFilter, + this.Positional, + this.Named + ); + + public Signature WithIsFilter(bool isFilter) => new Signature( + this.Name, + this.Description, + isFilter, + this.Positional, + this.Named + ); + } +} \ No newline at end of file From db2fb405eaa1c91c6a1c106b8881afb2738f5663 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Thu, 27 Feb 2020 22:52:11 -0500 Subject: [PATCH 3/9] sink is not a filter --- src/Nu.Plugin/NuPlugin.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index ed2ab1b..bb9f0f6 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -43,7 +43,6 @@ public NuPlugin Description(string description) public async Task SinkPluginAsync() where T : INuPluginSink, new() { - _signature = _signature.WithIsFilter(true); var sink = new T(); using (var standardInput = new StreamReader(_stdin, Console.InputEncoding)) @@ -97,7 +96,7 @@ public NuPlugin Description(string description) OkResponse(_signature); break; } - else if (_signature.IsFilter && request.Method == "begin_filter") + else if (request.Method == "begin_filter") { OkResponse(filter.BeginFilter()); } From 1d32749d2a4f625dc55bddc4eab84cc58dfa5d70 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Fri, 28 Feb 2020 08:01:35 -0500 Subject: [PATCH 4/9] refactor command handler --- src/Nu.Plugin/NuPlugin.cs | 97 ++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index bb9f0f6..06d431d 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -41,44 +41,55 @@ public NuPlugin Description(string description) return this; } - public async Task SinkPluginAsync() where T : INuPluginSink, new() + public async Task SinkPluginAsync() where TSinkPlugin : INuPluginSink, new() { - var sink = new T(); - - using (var standardInput = new StreamReader(_stdin, Console.InputEncoding)) - using (_standardOutputWriter = new StreamWriter(_stdout, Console.OutputEncoding)) + await CommandHandler((req, plugin) => { - _standardOutputWriter.AutoFlush = true; + if (req.Method == "config") + { + OkResponse(_signature); + } + else if (req.Method == "sink") + { + var requestParams = req.GetParams>(); + plugin.Sink(requestParams); + } - while (true) + return true; + }); + } + + public async Task FilterPluginAsync() where TFilterPlugin : INuPluginFilter, new() + { + await CommandHandler((req, plugin) => + { + if (req.Method == "config") { - var request = await standardInput.GetNextRequestAsync(); + return OkResponse(_signature); + } - if (request is null || !request.IsValid) { break; } + if (req.Method == "begin_filter") + { + OkResponse(plugin.BeginFilter()); + } + else if (req.Method == "filter") + { + var requestParams = req.GetParams(); - if (request.Method == "config") - { - OkResponse(_signature); - break; - } - else if (request.Method == "sink") - { - var requestParams = request.GetParams>(); - sink.Sink(requestParams); - break; - } - else - { - break; - } + RpcValueResponse(plugin.Filter(requestParams)); } - } + else if (req.Method == "end_filter") + { + return OkResponse(plugin.EndFilter()); + } + + return false; + }); } - public async Task FilterPluginAsync() where T : INuPluginFilter, new() + private async Task CommandHandler(Func done) where TPluginType : new() { - _signature = _signature.WithIsFilter(true); - var filter = new T(); + var plugin = new TPluginType(); using (var standardInput = new StreamReader(_stdin, Console.InputEncoding)) using (_standardOutputWriter = new StreamWriter(_stdout, Console.OutputEncoding)) @@ -91,27 +102,7 @@ public NuPlugin Description(string description) if (request is null || !request.IsValid) { break; } - if (request.Method == "config") - { - OkResponse(_signature); - break; - } - else if (request.Method == "begin_filter") - { - OkResponse(filter.BeginFilter()); - } - else if (request.Method == "filter") - { - var requestParams = request.GetParams(); - - RpcValueResponse(filter.Filter(requestParams)); - } - else if (request.Method == "end_filter") - { - OkResponse(filter.EndFilter()); - break; - } - else + if (done(request, plugin)) { break; } @@ -119,15 +110,17 @@ public NuPlugin Description(string description) } } - private void OkResponse(object response) => Response(new JsonRpcOkResponse(response)); + private bool OkResponse(object response) => Response(new JsonRpcOkResponse(response)); - private void RpcValueResponse(JsonRpcParams rpcParams) => Response(new JsonRpcValueResponse(rpcParams)); + private bool RpcValueResponse(JsonRpcParams rpcParams) => Response(new JsonRpcValueResponse(rpcParams)); - private void Response(JsonRpcResponse response) + private bool Response(JsonRpcResponse response) { var serializedResponse = JsonSerializer.Serialize(response); _standardOutputWriter.WriteLine(serializedResponse); + + return true; } } } From 2649544a03c33c3fcfed62ebf523d1e0b0f4f71e Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Fri, 28 Feb 2020 08:05:46 -0500 Subject: [PATCH 5/9] cleanup --- src/Nu.Plugin/NuPlugin.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index 06d431d..c451d9f 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -100,12 +100,9 @@ await CommandHandler((req, plugin) => { var request = await standardInput.GetNextRequestAsync(); - if (request is null || !request.IsValid) { break; } - - if (done(request, plugin)) - { - break; - } + if (request is null + || !request.IsValid + || done(request, plugin)) { break; } } } } From b469c08893972835c746e934f4f9b280bd80871c Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Sat, 29 Feb 2020 13:33:37 -0500 Subject: [PATCH 6/9] update plugin response model --- src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs | 9 ++++- src/Nu.Plugin/NuPlugin.cs | 51 ++++++++++++++++--------- src/Nu.Plugin/Signature.cs | 6 +-- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs b/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs index e317a22..457b530 100644 --- a/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs +++ b/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs @@ -38,7 +38,14 @@ public JsonRpcRequest(string json) public T GetParams() { - return JsonSerializer.Deserialize(_jsonDoc.RootElement.GetProperty("params").GetRawText()); + var json = _jsonDoc?.RootElement.GetProperty("params").GetRawText()?.Trim(); + + if (!string.IsNullOrEmpty(json)) + { + return JsonSerializer.Deserialize(json); + } + + return default(T); } public bool IsValid => _isValid; diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index c451d9f..bb5ad17 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -47,7 +47,7 @@ await CommandHandler((req, plugin) => { if (req.Method == "config") { - OkResponse(_signature); + Done(_signature); } else if (req.Method == "sink") { @@ -55,7 +55,7 @@ await CommandHandler((req, plugin) => plugin.Sink(requestParams); } - return true; + return Done(); }); } @@ -65,29 +65,28 @@ await CommandHandler((req, plugin) => { if (req.Method == "config") { - return OkResponse(_signature); + return Done(_signature.WithIsFilter(true)); } - - if (req.Method == "begin_filter") + else if (req.Method == "begin_filter") { - OkResponse(plugin.BeginFilter()); + return Continue(plugin.BeginFilter()); } else if (req.Method == "filter") { var requestParams = req.GetParams(); - RpcValueResponse(plugin.Filter(requestParams)); + return RpcValueResponse(plugin.Filter(requestParams)); } else if (req.Method == "end_filter") { - return OkResponse(plugin.EndFilter()); + return Done(plugin.EndFilter()); } - return false; + return Done(); }); } - private async Task CommandHandler(Func done) where TPluginType : new() + private async Task CommandHandler(Func response) where TPluginType : new() { var plugin = new TPluginType(); @@ -100,24 +99,42 @@ await CommandHandler((req, plugin) => { var request = await standardInput.GetNextRequestAsync(); - if (request is null - || !request.IsValid - || done(request, plugin)) { break; } + if (request?.IsValid != true) { break; } + + switch (response(request, plugin)) + { + case DoneResponse done: + break; + } } } } - private bool OkResponse(object response) => Response(new JsonRpcOkResponse(response)); + private PluginResponse Done(object response = null) => response == null + ? new DoneResponse() + : Response(new JsonRpcOkResponse(response), true); - private bool RpcValueResponse(JsonRpcParams rpcParams) => Response(new JsonRpcValueResponse(rpcParams)); + private PluginResponse Continue(object response) => Response(new JsonRpcOkResponse(response)); - private bool Response(JsonRpcResponse response) + private PluginResponse RpcValueResponse(JsonRpcParams rpcParams) => Response(new JsonRpcValueResponse(rpcParams)); + + private PluginResponse Response(JsonRpcResponse response, bool isDone = false) { var serializedResponse = JsonSerializer.Serialize(response); _standardOutputWriter.WriteLine(serializedResponse); - return true; + if (isDone) + { + return new DoneResponse(); + } + + return new ContinueResponse(); } + + internal abstract class PluginResponse { } + + internal class ContinueResponse : PluginResponse { } + internal class DoneResponse : PluginResponse { } } } diff --git a/src/Nu.Plugin/Signature.cs b/src/Nu.Plugin/Signature.cs index c2993c7..89dddee 100644 --- a/src/Nu.Plugin/Signature.cs +++ b/src/Nu.Plugin/Signature.cs @@ -28,16 +28,16 @@ private Signature(string name, string usage, bool isFilter, int[] positional, ob public int[] Positional { get; } = Array.Empty(); [JsonPropertyName("rest_positional")] - public int[] RestPositional { get; } = Array.Empty(); + public int[] RestPositional { get; } = null; [JsonPropertyName("named")] public object Named { get; } = new { }; [JsonPropertyName("yields")] - public object Yields { get; } = new { }; + public object Yields { get; } = null; [JsonPropertyName("input")] - public object Input { get; } = new { }; + public object Input { get; } = null; [JsonPropertyName("is_filter")] public bool IsFilter { get; } = false; From fa98124d763482b4e16e39eb587e6cdf12460b53 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Sun, 1 Mar 2020 12:41:01 -0500 Subject: [PATCH 7/9] nuplugin command handler --- src/Nu.Plugin/JsonRpc/JsonRpcOkResponse.cs | 12 --- src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs | 20 +++- src/Nu.Plugin/JsonRpc/JsonRpcValueResponse.cs | 18 ---- src/Nu.Plugin/NuPlugin.PluginHandler.cs | 68 +++++++++++++ src/Nu.Plugin/NuPlugin.PluginResponse.cs | 95 +++++++++++++++++++ src/Nu.Plugin/NuPlugin.cs | 83 ++++------------ 6 files changed, 197 insertions(+), 99 deletions(-) delete mode 100644 src/Nu.Plugin/JsonRpc/JsonRpcOkResponse.cs delete mode 100644 src/Nu.Plugin/JsonRpc/JsonRpcValueResponse.cs create mode 100644 src/Nu.Plugin/NuPlugin.PluginHandler.cs create mode 100644 src/Nu.Plugin/NuPlugin.PluginResponse.cs diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcOkResponse.cs b/src/Nu.Plugin/JsonRpc/JsonRpcOkResponse.cs deleted file mode 100644 index e292d46..0000000 --- a/src/Nu.Plugin/JsonRpc/JsonRpcOkResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Nu.Plugin -{ - internal class JsonRpcOkResponse : JsonRpcResponse - { - public JsonRpcOkResponse(object okResponseParams) => Params = new - { - Ok = okResponseParams - }; - - public override object Params { get; } - } -} \ No newline at end of file diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs b/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs index 997f997..a166d11 100644 --- a/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs +++ b/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs @@ -4,8 +4,10 @@ namespace Nu.Plugin { - internal abstract class JsonRpcResponse + internal class JsonRpcResponse { + public JsonRpcResponse(object rpcResponseParams) => Params = rpcResponseParams; + [JsonPropertyName("jsonrpc")] public string JsonRPC { get; } = "2.0"; @@ -13,6 +15,20 @@ internal abstract class JsonRpcResponse public string Method { get; } = "response"; [JsonPropertyName("params")] - public abstract object Params { get; } + public object Params { get; } + + public static JsonRpcResponse Ok(object okResponseParams) => + new JsonRpcResponse(new { + Ok = okResponseParams + }); + + public static JsonRpcResponse RpcValue(object rpcParams) => + Ok(new object[] { + new { + Ok = new { + Value = rpcParams + } + } + }); } } \ No newline at end of file diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcValueResponse.cs b/src/Nu.Plugin/JsonRpc/JsonRpcValueResponse.cs deleted file mode 100644 index 2ca63ef..0000000 --- a/src/Nu.Plugin/JsonRpc/JsonRpcValueResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Nu.Plugin -{ - internal class JsonRpcValueResponse : JsonRpcResponse - { - public JsonRpcValueResponse(JsonRpcParams rpcParams) => Params = new - { - Ok = new object[] { - new { - Ok = new { - Value = rpcParams - } - } - } - }; - - public override object Params { get; } - } -} \ No newline at end of file diff --git a/src/Nu.Plugin/NuPlugin.PluginHandler.cs b/src/Nu.Plugin/NuPlugin.PluginHandler.cs new file mode 100644 index 0000000..1cb510b --- /dev/null +++ b/src/Nu.Plugin/NuPlugin.PluginHandler.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Nu.Plugin +{ + public partial class NuPlugin + { + private async Task CommandHandler( + Action> pluginRes + ) where TPluginType : new() + { + using (var handler = PluginHandler.Create(_stdin, _stdout)) + while (await handler.HandleNextRequestAsync(pluginRes)) ; + } + + internal class PluginHandler : IDisposable + where TPluginType : new() + { + private readonly StreamReader _stdinReader; + private readonly StreamWriter _stdoutWriter; + private readonly TPluginType _plugin; + + private PluginHandler(Stream stdin, Stream stdout) + { + _stdinReader = new StreamReader(stdin, Console.InputEncoding); + _stdoutWriter = new StreamWriter(stdout, Console.OutputEncoding) { AutoFlush = true }; + + _plugin = new TPluginType(); + } + + public static PluginHandler Create(Stream stdin, Stream stdout) + { + var handler = new PluginHandler(stdin, stdout); + + return handler; + } + + internal async Task HandleNextRequestAsync( + Action> pluginRes + ) + { + var json = await _stdinReader.ReadLineAsync(); + + if (!string.IsNullOrEmpty(json?.Trim())) + { + var request = new JsonRpcRequest(json); + if (request?.IsValid == true) + { + var responseHandler = new PluginResponse(_stdoutWriter, _plugin); + + pluginRes(request, responseHandler); + + return await responseHandler.RespondAsync(); + } + } + + return false; + } + + public void Dispose() + { + _stdinReader?.Dispose(); + _stdoutWriter?.Dispose(); + } + } + } +} diff --git a/src/Nu.Plugin/NuPlugin.PluginResponse.cs b/src/Nu.Plugin/NuPlugin.PluginResponse.cs new file mode 100644 index 0000000..ddc621d --- /dev/null +++ b/src/Nu.Plugin/NuPlugin.PluginResponse.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Nu.Plugin.Interfaces; + +namespace Nu.Plugin +{ + public partial class NuPlugin + { + internal class PluginResponse where TPluginType : new() + { + readonly TaskCompletionSource _tcs = new TaskCompletionSource(); + readonly StreamWriter _writer; + readonly TPluginType _plugin; + + public PluginResponse(StreamWriter writer, TPluginType plugin) + { + _plugin = plugin; + _writer = writer; + } + + public void Config(Signature signature) + { + var rpcResponse = JsonRpcResponse.Ok(signature); + + Respond(rpcResponse); + Done(); + } + + public void Sink(IEnumerable requestParams) + { + if (_plugin is INuPluginSink sinkPlugin) + { + sinkPlugin.Sink(requestParams); + } + + Done(); + } + + public void BeginFilter() + { + if (_plugin is INuPluginFilter filterPlugin) + { + var rpcResponse = JsonRpcResponse.Ok( + filterPlugin.BeginFilter() + ); + + Respond(rpcResponse); + } + + Continue(); + } + + public void Filter(JsonRpcParams requestParams) + { + if (_plugin is INuPluginFilter filterPlugin) + { + Respond( + JsonRpcResponse.RpcValue( + filterPlugin.Filter(requestParams) + ) + ); + } + + Continue(); + } + + public void EndFilter() + { + if (_plugin is INuPluginFilter filterPlugin) + { + var rpcResponse = JsonRpcResponse.Ok( + filterPlugin.EndFilter() + ); + + Respond(rpcResponse); + } + + Continue(); + } + + public void Quit() => Done(); + + private void Respond(JsonRpcResponse rpcResponse) => + _writer.WriteLine(JsonSerializer.Serialize(rpcResponse)); + + private void Continue() => _tcs.SetResult(true); + + private void Done() => _tcs.SetResult(false); + + public Task RespondAsync() => _tcs.Task; + } + } +} diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index bb5ad17..53b1b8f 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -1,19 +1,17 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text.Json; using System.Threading.Tasks; using Nu.Plugin.Interfaces; namespace Nu.Plugin { - public class NuPlugin + public partial class NuPlugin { private readonly Stream _stdin; private readonly Stream _stdout; private Signature _signature = Signature.Create(); - private StreamWriter _standardOutputWriter; private NuPlugin(Stream stdin, Stream stdout) { @@ -43,98 +41,49 @@ public NuPlugin Description(string description) public async Task SinkPluginAsync() where TSinkPlugin : INuPluginSink, new() { - await CommandHandler((req, plugin) => + await CommandHandler((req, res) => { if (req.Method == "config") { - Done(_signature); + res.Config(_signature); } else if (req.Method == "sink") { var requestParams = req.GetParams>(); - plugin.Sink(requestParams); + res.Sink(requestParams); + } + else + { + res.Quit(); } - - return Done(); }); } public async Task FilterPluginAsync() where TFilterPlugin : INuPluginFilter, new() { - await CommandHandler((req, plugin) => + await CommandHandler((req, res) => { if (req.Method == "config") { - return Done(_signature.WithIsFilter(true)); + res.Config(_signature.WithIsFilter(true)); } else if (req.Method == "begin_filter") { - return Continue(plugin.BeginFilter()); + res.BeginFilter(); } else if (req.Method == "filter") { - var requestParams = req.GetParams(); - - return RpcValueResponse(plugin.Filter(requestParams)); + res.Filter(req.GetParams()); } else if (req.Method == "end_filter") { - return Done(plugin.EndFilter()); + res.EndFilter(); } - - return Done(); - }); - } - - private async Task CommandHandler(Func response) where TPluginType : new() - { - var plugin = new TPluginType(); - - using (var standardInput = new StreamReader(_stdin, Console.InputEncoding)) - using (_standardOutputWriter = new StreamWriter(_stdout, Console.OutputEncoding)) - { - _standardOutputWriter.AutoFlush = true; - - while (true) + else { - var request = await standardInput.GetNextRequestAsync(); - - if (request?.IsValid != true) { break; } - - switch (response(request, plugin)) - { - case DoneResponse done: - break; - } + res.Quit(); } - } - } - - private PluginResponse Done(object response = null) => response == null - ? new DoneResponse() - : Response(new JsonRpcOkResponse(response), true); - - private PluginResponse Continue(object response) => Response(new JsonRpcOkResponse(response)); - - private PluginResponse RpcValueResponse(JsonRpcParams rpcParams) => Response(new JsonRpcValueResponse(rpcParams)); - - private PluginResponse Response(JsonRpcResponse response, bool isDone = false) - { - var serializedResponse = JsonSerializer.Serialize(response); - - _standardOutputWriter.WriteLine(serializedResponse); - - if (isDone) - { - return new DoneResponse(); - } - - return new ContinueResponse(); + }); } - - internal abstract class PluginResponse { } - - internal class ContinueResponse : PluginResponse { } - internal class DoneResponse : PluginResponse { } } } From cec9de54771adae31786aab422ab7de59aed1e07 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Sun, 1 Mar 2020 12:58:24 -0500 Subject: [PATCH 8/9] PluginResponse cleanup --- src/Nu.Plugin/NuPlugin.PluginResponse.cs | 23 ++++++++++++----------- src/Nu.Plugin/NuPlugin.cs | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Nu.Plugin/NuPlugin.PluginResponse.cs b/src/Nu.Plugin/NuPlugin.PluginResponse.cs index ddc621d..e3f3266 100644 --- a/src/Nu.Plugin/NuPlugin.PluginResponse.cs +++ b/src/Nu.Plugin/NuPlugin.PluginResponse.cs @@ -22,9 +22,10 @@ public PluginResponse(StreamWriter writer, TPluginType plugin) public void Config(Signature signature) { - var rpcResponse = JsonRpcResponse.Ok(signature); + Respond( + JsonRpcResponse.Ok(signature) + ); - Respond(rpcResponse); Done(); } @@ -42,11 +43,11 @@ public void BeginFilter() { if (_plugin is INuPluginFilter filterPlugin) { - var rpcResponse = JsonRpcResponse.Ok( - filterPlugin.BeginFilter() + Respond( + JsonRpcResponse.Ok( + filterPlugin.BeginFilter() + ) ); - - Respond(rpcResponse); } Continue(); @@ -70,14 +71,14 @@ public void EndFilter() { if (_plugin is INuPluginFilter filterPlugin) { - var rpcResponse = JsonRpcResponse.Ok( - filterPlugin.EndFilter() + Respond( + JsonRpcResponse.Ok( + filterPlugin.EndFilter() + ) ); - - Respond(rpcResponse); } - Continue(); + Done(); } public void Quit() => Done(); diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index 53b1b8f..ce5cfc4 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -19,7 +19,7 @@ private NuPlugin(Stream stdin, Stream stdout) _stdin = stdin; } - public static NuPlugin Build(string name = null) + public static NuPlugin Build(string name) { var stdin = Console.OpenStandardInput(); var stdout = Console.OpenStandardOutput(); From ba14e5bb72eb21bb8799765dbe4924765d6c3962 Mon Sep 17 00:00:00 2001 From: Michael Tyson Date: Thu, 12 Mar 2020 23:24:06 -0400 Subject: [PATCH 9/9] Config Signature - Nushell JsonRPC protocol (#2) * cleanup * add tests * update nu plugin class * signature class cleanup * more tests * AddNamedSwitch, AddNamedOptional * Fix project * Add named switch tests * add named parameter types * add positional arguments * Result and ReturnSuccess types --- .editorconfig | 214 ++++++++++++++++++ .vscode/settings.json | 4 + nu_plugin_len.sln => nu-plugin-lib.sln | 14 ++ samples/Nu.Plugin.Len/LengthFilter.cs | 40 ++++ samples/Nu.Plugin.Len/Program.cs | 33 +-- .../Extensions/StreamReaderExtension.cs | 9 +- src/Nu.Plugin/Interfaces/INuPluginFilter.cs | 11 +- src/Nu.Plugin/Interfaces/INuPluginSink.cs | 4 +- src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs | 8 +- src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs | 26 +-- .../{JsonRpcParams.cs => JsonRpcValue.cs} | 4 +- src/Nu.Plugin/JsonRpc/Result.cs | 43 ++++ src/Nu.Plugin/JsonRpc/ReturnSuccess.cs | 35 +++ src/Nu.Plugin/NamedType.cs | 32 +++ src/Nu.Plugin/Nu.Plugin.csproj | 6 + src/Nu.Plugin/NuPlugin.PluginHandler.cs | 68 ------ src/Nu.Plugin/NuPlugin.PluginResponse.cs | 96 -------- src/Nu.Plugin/NuPlugin.cs | 142 ++++++++---- src/Nu.Plugin/PluginHandler.cs | 49 ++++ src/Nu.Plugin/PluginResponse.cs | 79 +++++++ src/Nu.Plugin/PositionalType.cs | 24 ++ src/Nu.Plugin/Signature.cs | 201 ++++++++++++++-- src/Nu.Plugin/SyntaxShape.cs | 39 ++++ tests/Nu.Plugin.Tests/Nu.Plugin.Tests.csproj | 20 ++ tests/Nu.Plugin.Tests/NuPluginTests.cs | 29 +++ tests/Nu.Plugin.Tests/SignatureTests.cs | 147 ++++++++++++ 26 files changed, 1090 insertions(+), 287 deletions(-) create mode 100644 .editorconfig create mode 100644 .vscode/settings.json rename nu_plugin_len.sln => nu-plugin-lib.sln (71%) create mode 100644 samples/Nu.Plugin.Len/LengthFilter.cs rename src/Nu.Plugin/JsonRpc/{JsonRpcParams.cs => JsonRpcValue.cs} (96%) create mode 100644 src/Nu.Plugin/JsonRpc/Result.cs create mode 100644 src/Nu.Plugin/JsonRpc/ReturnSuccess.cs create mode 100644 src/Nu.Plugin/NamedType.cs delete mode 100644 src/Nu.Plugin/NuPlugin.PluginHandler.cs delete mode 100644 src/Nu.Plugin/NuPlugin.PluginResponse.cs create mode 100644 src/Nu.Plugin/PluginHandler.cs create mode 100644 src/Nu.Plugin/PluginResponse.cs create mode 100644 src/Nu.Plugin/PositionalType.cs create mode 100644 src/Nu.Plugin/SyntaxShape.cs create mode 100644 tests/Nu.Plugin.Tests/Nu.Plugin.Tests.csproj create mode 100644 tests/Nu.Plugin.Tests/NuPluginTests.cs create mode 100644 tests/Nu.Plugin.Tests/SignatureTests.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..17605f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,214 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Powershell files +[*.ps1] +indent_size = 2 + +# Shell script files +[*.sh] +end_of_line = lf +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:refactoring +dotnet_style_qualification_for_property = false:refactoring +dotnet_style_qualification_for_method = false:refactoring +dotnet_style_qualification_for_event = false:refactoring + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# warning RS0037: PublicAPI.txt is missing '#nullable enable' +dotnet_diagnostic.RS0037.severity = none + +[src/CodeStyle/**.{cs,vb}] +# warning RS0005: Do not use generic CodeAction.Create to create CodeAction +dotnet_diagnostic.RS0005.severity = none \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..abe5944 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableRoslynAnalyzers": true +} \ No newline at end of file diff --git a/nu_plugin_len.sln b/nu-plugin-lib.sln similarity index 71% rename from nu_plugin_len.sln rename to nu-plugin-lib.sln index cc0add4..d18a0d7 100644 --- a/nu_plugin_len.sln +++ b/nu-plugin-lib.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "samples\Nu.Plugin.Len", "sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "src\Nu.Plugin", "src\Nu.Plugin\Nu.Plugin.csproj", "{A5740DCA-A481-4F61-A296-BC9733C9225E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nu.Plugin.Tests", "tests\Nu.Plugin.Tests\Nu.Plugin.Tests.csproj", "{31641E0A-9E5B-460D-98DA-E5DDEF00F91C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,5 +46,17 @@ Global {A5740DCA-A481-4F61-A296-BC9733C9225E}.Release|x64.Build.0 = Release|Any CPU {A5740DCA-A481-4F61-A296-BC9733C9225E}.Release|x86.ActiveCfg = Release|Any CPU {A5740DCA-A481-4F61-A296-BC9733C9225E}.Release|x86.Build.0 = Release|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Debug|x64.ActiveCfg = Debug|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Debug|x64.Build.0 = Debug|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Debug|x86.ActiveCfg = Debug|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Debug|x86.Build.0 = Debug|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Release|Any CPU.Build.0 = Release|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Release|x64.ActiveCfg = Release|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Release|x64.Build.0 = Release|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Release|x86.ActiveCfg = Release|Any CPU + {31641E0A-9E5B-460D-98DA-E5DDEF00F91C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/samples/Nu.Plugin.Len/LengthFilter.cs b/samples/Nu.Plugin.Len/LengthFilter.cs new file mode 100644 index 0000000..99c66d4 --- /dev/null +++ b/samples/Nu.Plugin.Len/LengthFilter.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System; +using System.Collections.Generic; +using Nu.Plugin.Interfaces; +using Nu.Plugin.JsonRpc; + +namespace Nu.Plugin.Len +{ + public class LengthFilter : INuPluginFilter + { + public Result>> BeginFilter() + { + return new OkResult>>(Enumerable.Empty>()); + } + + public Result>> Filter(JsonRpcValue requestParams) + { + var stringLength = requestParams.Value.Primitive["String"].ToString().Length; + + requestParams.Value.Primitive = new Dictionary + { + {"Int", stringLength} + }; + + return new OkResult>>( + new OkResult[] + { + new OkResult( + new ValueReturnSuccess(requestParams) + ) + } + ); + } + + public Result>> EndFilter() + { + return new OkResult>>(Enumerable.Empty>()); + } + } +} diff --git a/samples/Nu.Plugin.Len/Program.cs b/samples/Nu.Plugin.Len/Program.cs index 8945464..13e493a 100644 --- a/samples/Nu.Plugin.Len/Program.cs +++ b/samples/Nu.Plugin.Len/Program.cs @@ -1,30 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Nu.Plugin.Interfaces; +using System.Threading.Tasks; namespace Nu.Plugin.Len { - class Program : INuPluginFilter + internal static class Program { - static async Task Main() => await NuPlugin + private static async Task Main() => await NuPlugin .Build("len") .Description("Return the length of a string") - .FilterPluginAsync(); - - public object BeginFilter() => Array.Empty(); - - public JsonRpcParams Filter(JsonRpcParams requestParams) - { - var stringLength = requestParams.Value.Primitive["String"].ToString().Length; - - requestParams.Value.Primitive = new Dictionary{ - {"Int", stringLength} - }; - - return requestParams; - } - - public object EndFilter() => Array.Empty(); + .Required(SyntaxShape.String, "required_positional", "required positional description") + .Optional(SyntaxShape.Int, "optional_positional_1", "optional positional description #1") + .Optional(SyntaxShape.Any, "optional_positional_2", "optional positional description #2") + .Switch("all", "All of everything") + .Named(SyntaxShape.Any, "copy", "copy description") + .RequiredNamed(SyntaxShape.String, "required", "required description") + .Rest("test rest arguments") + .FilterAsync(); } } diff --git a/src/Nu.Plugin/Extensions/StreamReaderExtension.cs b/src/Nu.Plugin/Extensions/StreamReaderExtension.cs index 7b7436a..9e796bf 100644 --- a/src/Nu.Plugin/Extensions/StreamReaderExtension.cs +++ b/src/Nu.Plugin/Extensions/StreamReaderExtension.cs @@ -9,12 +9,7 @@ internal static async Task GetNextRequestAsync(this StreamReader { var json = await reader.ReadLineAsync(); - if (!string.IsNullOrEmpty(json?.Trim())) - { - return new JsonRpcRequest(json); - } - - return null; + return !string.IsNullOrEmpty(json?.Trim()) ? new JsonRpcRequest(json) : null; } } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/Interfaces/INuPluginFilter.cs b/src/Nu.Plugin/Interfaces/INuPluginFilter.cs index 5cf57fd..9ac96a0 100644 --- a/src/Nu.Plugin/Interfaces/INuPluginFilter.cs +++ b/src/Nu.Plugin/Interfaces/INuPluginFilter.cs @@ -1,9 +1,12 @@ +using System.Collections.Generic; +using Nu.Plugin.JsonRpc; + namespace Nu.Plugin.Interfaces { public interface INuPluginFilter { - object BeginFilter(); - JsonRpcParams Filter(JsonRpcParams requestParams); - object EndFilter(); + Result>> BeginFilter(); + Result>> Filter(JsonRpcValue requestParams); + Result>> EndFilter(); } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/Interfaces/INuPluginSink.cs b/src/Nu.Plugin/Interfaces/INuPluginSink.cs index 8603b22..ae3886b 100644 --- a/src/Nu.Plugin/Interfaces/INuPluginSink.cs +++ b/src/Nu.Plugin/Interfaces/INuPluginSink.cs @@ -4,6 +4,6 @@ namespace Nu.Plugin.Interfaces { public interface INuPluginSink { - void Sink(IEnumerable requestParams); + void Sink(IEnumerable requestParams); } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs b/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs index 457b530..8e97faa 100644 --- a/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs +++ b/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs @@ -4,8 +4,8 @@ namespace Nu.Plugin { internal class JsonRpcRequest { - readonly JsonDocument _jsonDoc; - readonly bool _isValid = true; + private readonly JsonDocument _jsonDoc; + private readonly bool _isValid = true; public JsonRpcRequest(string json) { @@ -45,9 +45,9 @@ public T GetParams() return JsonSerializer.Deserialize(json); } - return default(T); + return default; } public bool IsValid => _isValid; } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs b/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs index a166d11..146a148 100644 --- a/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs +++ b/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs @@ -1,6 +1,7 @@ -using System; -using System.Text.Json; +using System.Linq; +using System.Collections.Generic; using System.Text.Json.Serialization; +using Nu.Plugin.JsonRpc; namespace Nu.Plugin { @@ -9,7 +10,7 @@ internal class JsonRpcResponse public JsonRpcResponse(object rpcResponseParams) => Params = rpcResponseParams; [JsonPropertyName("jsonrpc")] - public string JsonRPC { get; } = "2.0"; + public string JsonRpc { get; } = "2.0"; [JsonPropertyName("method")] public string Method { get; } = "response"; @@ -17,18 +18,9 @@ internal class JsonRpcResponse [JsonPropertyName("params")] public object Params { get; } - public static JsonRpcResponse Ok(object okResponseParams) => - new JsonRpcResponse(new { - Ok = okResponseParams - }); - - public static JsonRpcResponse RpcValue(object rpcParams) => - Ok(new object[] { - new { - Ok = new { - Value = rpcParams - } - } - }); + public static JsonRpcResponse Ok(Result result) => new JsonRpcResponse(result); + public static JsonRpcResponse Ok(object okResponseParams) => Ok(new OkResult(okResponseParams)); + public static JsonRpcResponse Ok(IEnumerable rpcValues) => Ok(rpcValues.Select(v => new OkResult(v))); + public static JsonRpcResponse Ok(JsonRpcValue rpcValue) => Ok(new JsonRpcValue[] { rpcValue }); } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/JsonRpc/JsonRpcParams.cs b/src/Nu.Plugin/JsonRpc/JsonRpcValue.cs similarity index 96% rename from src/Nu.Plugin/JsonRpc/JsonRpcParams.cs rename to src/Nu.Plugin/JsonRpc/JsonRpcValue.cs index 656a9fa..564ec13 100644 --- a/src/Nu.Plugin/JsonRpc/JsonRpcParams.cs +++ b/src/Nu.Plugin/JsonRpc/JsonRpcValue.cs @@ -3,7 +3,7 @@ namespace Nu.Plugin { - public class JsonRpcParams + public class JsonRpcValue { [JsonPropertyName("value")] public ParamValue Value { get; set; } @@ -34,4 +34,4 @@ public class ParamTagSpan } } } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/JsonRpc/Result.cs b/src/Nu.Plugin/JsonRpc/Result.cs new file mode 100644 index 0000000..e0ea05a --- /dev/null +++ b/src/Nu.Plugin/JsonRpc/Result.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Serialization; + +namespace Nu.Plugin.JsonRpc +{ + public abstract class Result + { + public Result(object result) => Value = result; + + [JsonIgnore] + public object Value { get; } + } + public abstract class Result : Result + { + public Result(T result) : base(result) { } + + [JsonIgnore] + public new T Value => (T)base.Value; + } + + public class OkResult : Result + { + public OkResult(object result) : base(result) { } + + [JsonPropertyName("Ok")] + public new object Value => base.Value; + } + + public class OkResult : Result + { + public OkResult(T result) : base(result) { } + + [JsonPropertyName("Ok")] + public new T Value => base.Value; + } + + public class ErrResult : Result + { + public ErrResult(object result) : base(result) { } + + [JsonPropertyName("Err")] + public new object Value => base.Value; + } +} diff --git a/src/Nu.Plugin/JsonRpc/ReturnSuccess.cs b/src/Nu.Plugin/JsonRpc/ReturnSuccess.cs new file mode 100644 index 0000000..faecf23 --- /dev/null +++ b/src/Nu.Plugin/JsonRpc/ReturnSuccess.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace Nu.Plugin.JsonRpc +{ + public interface IReturnSuccess { } + + public abstract class ReturnSuccessBase : IReturnSuccess + { + public ReturnSuccessBase(JsonRpcValue value) => Value = value; + + public JsonRpcValue Value { get; } + } + + public class ValueReturnSuccess : ReturnSuccessBase + { + public ValueReturnSuccess(JsonRpcValue value) : base(value) { } + + [JsonPropertyName("Value")] + public new JsonRpcValue Value => base.Value; + } + + public class DebugValueReturnSuccess : ReturnSuccessBase + { + public DebugValueReturnSuccess(JsonRpcValue value) : base(value) { } + + [JsonPropertyName("DebugValue")] + public new JsonRpcValue Value => base.Value; + } + + public class ActionReturnSuccess : IReturnSuccess + { + [JsonPropertyName("Action")] + public object Action => null; + } +} diff --git a/src/Nu.Plugin/NamedType.cs b/src/Nu.Plugin/NamedType.cs new file mode 100644 index 0000000..94b40f8 --- /dev/null +++ b/src/Nu.Plugin/NamedType.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace Nu.Plugin +{ + public interface INamedType { } + + public class NamedTypeSwitch : INamedType + { + public NamedTypeSwitch(char switchChar) => Switch = switchChar; + + [JsonPropertyName("Switch")] + public char Switch { get; } + } + + public class NamedTypeMandatory : INamedType + { + public NamedTypeMandatory(string flagValue) : this(flagValue, SyntaxShape.Any) { } + public NamedTypeMandatory(string flagValue, SyntaxShape syntax) => Mandatory = new string[] { flagValue, syntax.Shape }; + + [JsonPropertyName("Mandatory")] + public string[] Mandatory { get; } + } + + public class NamedTypeOptional : INamedType + { + public NamedTypeOptional(string flagValue) : this(flagValue, SyntaxShape.Any) { } + public NamedTypeOptional(string flagValue, SyntaxShape syntax) => Optional = new string[] { flagValue, syntax.Shape }; + + [JsonPropertyName("Optional")] + public string[] Optional { get; } + } +} diff --git a/src/Nu.Plugin/Nu.Plugin.csproj b/src/Nu.Plugin/Nu.Plugin.csproj index b52060a..919a990 100644 --- a/src/Nu.Plugin/Nu.Plugin.csproj +++ b/src/Nu.Plugin/Nu.Plugin.csproj @@ -15,5 +15,11 @@ + + + + <_Parameter1>$(MSBuildProjectName).Tests + + diff --git a/src/Nu.Plugin/NuPlugin.PluginHandler.cs b/src/Nu.Plugin/NuPlugin.PluginHandler.cs deleted file mode 100644 index 1cb510b..0000000 --- a/src/Nu.Plugin/NuPlugin.PluginHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Nu.Plugin -{ - public partial class NuPlugin - { - private async Task CommandHandler( - Action> pluginRes - ) where TPluginType : new() - { - using (var handler = PluginHandler.Create(_stdin, _stdout)) - while (await handler.HandleNextRequestAsync(pluginRes)) ; - } - - internal class PluginHandler : IDisposable - where TPluginType : new() - { - private readonly StreamReader _stdinReader; - private readonly StreamWriter _stdoutWriter; - private readonly TPluginType _plugin; - - private PluginHandler(Stream stdin, Stream stdout) - { - _stdinReader = new StreamReader(stdin, Console.InputEncoding); - _stdoutWriter = new StreamWriter(stdout, Console.OutputEncoding) { AutoFlush = true }; - - _plugin = new TPluginType(); - } - - public static PluginHandler Create(Stream stdin, Stream stdout) - { - var handler = new PluginHandler(stdin, stdout); - - return handler; - } - - internal async Task HandleNextRequestAsync( - Action> pluginRes - ) - { - var json = await _stdinReader.ReadLineAsync(); - - if (!string.IsNullOrEmpty(json?.Trim())) - { - var request = new JsonRpcRequest(json); - if (request?.IsValid == true) - { - var responseHandler = new PluginResponse(_stdoutWriter, _plugin); - - pluginRes(request, responseHandler); - - return await responseHandler.RespondAsync(); - } - } - - return false; - } - - public void Dispose() - { - _stdinReader?.Dispose(); - _stdoutWriter?.Dispose(); - } - } - } -} diff --git a/src/Nu.Plugin/NuPlugin.PluginResponse.cs b/src/Nu.Plugin/NuPlugin.PluginResponse.cs deleted file mode 100644 index e3f3266..0000000 --- a/src/Nu.Plugin/NuPlugin.PluginResponse.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using System.Threading.Tasks; -using Nu.Plugin.Interfaces; - -namespace Nu.Plugin -{ - public partial class NuPlugin - { - internal class PluginResponse where TPluginType : new() - { - readonly TaskCompletionSource _tcs = new TaskCompletionSource(); - readonly StreamWriter _writer; - readonly TPluginType _plugin; - - public PluginResponse(StreamWriter writer, TPluginType plugin) - { - _plugin = plugin; - _writer = writer; - } - - public void Config(Signature signature) - { - Respond( - JsonRpcResponse.Ok(signature) - ); - - Done(); - } - - public void Sink(IEnumerable requestParams) - { - if (_plugin is INuPluginSink sinkPlugin) - { - sinkPlugin.Sink(requestParams); - } - - Done(); - } - - public void BeginFilter() - { - if (_plugin is INuPluginFilter filterPlugin) - { - Respond( - JsonRpcResponse.Ok( - filterPlugin.BeginFilter() - ) - ); - } - - Continue(); - } - - public void Filter(JsonRpcParams requestParams) - { - if (_plugin is INuPluginFilter filterPlugin) - { - Respond( - JsonRpcResponse.RpcValue( - filterPlugin.Filter(requestParams) - ) - ); - } - - Continue(); - } - - public void EndFilter() - { - if (_plugin is INuPluginFilter filterPlugin) - { - Respond( - JsonRpcResponse.Ok( - filterPlugin.EndFilter() - ) - ); - } - - Done(); - } - - public void Quit() => Done(); - - private void Respond(JsonRpcResponse rpcResponse) => - _writer.WriteLine(JsonSerializer.Serialize(rpcResponse)); - - private void Continue() => _tcs.SetResult(true); - - private void Done() => _tcs.SetResult(false); - - public Task RespondAsync() => _tcs.Task; - } - } -} diff --git a/src/Nu.Plugin/NuPlugin.cs b/src/Nu.Plugin/NuPlugin.cs index ce5cfc4..0488097 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -6,17 +6,17 @@ namespace Nu.Plugin { - public partial class NuPlugin + public class NuPlugin { private readonly Stream _stdin; private readonly Stream _stdout; - private Signature _signature = Signature.Create(); - private NuPlugin(Stream stdin, Stream stdout) + private NuPlugin(Stream stdin, Stream stdout, string name) { _stdout = stdout; _stdin = stdin; + _signature = _signature.WithName(name); } public static NuPlugin Build(string name) @@ -24,66 +24,118 @@ public static NuPlugin Build(string name) var stdin = Console.OpenStandardInput(); var stdout = Console.OpenStandardOutput(); - return new NuPlugin(stdin, stdout).Name(name); + return new NuPlugin(stdin, stdout, name); } - public NuPlugin Name(string name) + public NuPlugin Description(string description) { - _signature = _signature.WithName(name); + _signature = _signature.WithDescription(description); return this; } - public NuPlugin Description(string description) + public NuPlugin Required(SyntaxShape syntaxShape, string name, string description) { - _signature = _signature.WithDescription(description); + _signature = _signature.AddRequiredPositional(syntaxShape, name, description); return this; } - public async Task SinkPluginAsync() where TSinkPlugin : INuPluginSink, new() + public NuPlugin Optional(SyntaxShape syntaxShape, string name, string description) { - await CommandHandler((req, res) => - { - if (req.Method == "config") - { - res.Config(_signature); - } - else if (req.Method == "sink") - { - var requestParams = req.GetParams>(); - res.Sink(requestParams); - } - else - { - res.Quit(); - } - }); + _signature = _signature.AddOptionalPositional(syntaxShape, name, description); + return this; } - public async Task FilterPluginAsync() where TFilterPlugin : INuPluginFilter, new() + public NuPlugin Switch(string name, string description, char? flag = null) { - await CommandHandler((req, res) => - { - if (req.Method == "config") - { - res.Config(_signature.WithIsFilter(true)); - } - else if (req.Method == "begin_filter") - { - res.BeginFilter(); - } - else if (req.Method == "filter") - { - res.Filter(req.GetParams()); - } - else if (req.Method == "end_filter") + _signature = _signature.AddSwitch(name, description, flag); + return this; + } + + public NuPlugin Named(SyntaxShape syntaxShape, string name, string description, char? flag = null) + { + _signature = _signature.AddOptionalNamed(syntaxShape, name, description, flag); + return this; + } + + public NuPlugin RequiredNamed(SyntaxShape syntaxShape, string name, string description, char? flag = null) + { + _signature = _signature.AddRequiredNamed(syntaxShape, name, description, flag); + return this; + } + + public NuPlugin Rest(string description, SyntaxShape syntaxShape = null) + { + _signature = _signature.AddRestPositionalArguments(description, syntaxShape); + return this; + } + + public NuPlugin Yields() + { + throw new NotImplementedException("To be implemented in the future"); + } + + public NuPlugin Input() + { + throw new NotImplementedException("To be implemented in the future"); + } + + public async Task SinkAsync() where TSinkPlugin : INuPluginSink, new() + { + await CommandHandler( + (req, res) => { - res.EndFilter(); + switch (req.Method) + { + case "config": + res.Config(_signature); + break; + case "sink": + { + var requestParams = req.GetParams>(); + res.Sink(requestParams); + break; + } + default: + res.Quit(); + break; + } } - else + ); + } + + public async Task FilterAsync() where TFilterPlugin : INuPluginFilter, new() + { + await CommandHandler( + (req, res) => { - res.Quit(); + switch (req.Method) + { + case "config": + res.Config(_signature.WithIsFilter(true)); + break; + case "begin_filter": + res.BeginFilter(); + break; + case "filter": + res.Filter(req.GetParams()); + break; + case "end_filter": + res.EndFilter(); + break; + default: + res.Quit(); + break; + } } - }); + ); + } + + private async Task CommandHandler( + Action> pluginRes + ) where TPluginType : new() + { + using var handler = PluginHandler.Create(_stdin, _stdout); + while (await handler.HandleNextRequestAsync(pluginRes)) { } } } } diff --git a/src/Nu.Plugin/PluginHandler.cs b/src/Nu.Plugin/PluginHandler.cs new file mode 100644 index 0000000..2579da9 --- /dev/null +++ b/src/Nu.Plugin/PluginHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Nu.Plugin +{ + internal class PluginHandler : IDisposable + where TPluginType : new() + { + private readonly TPluginType _plugin; + private readonly StreamReader _stdinReader; + private readonly StreamWriter _stdoutWriter; + + private PluginHandler(Stream stdin, Stream stdout) + { + _plugin = new TPluginType(); + _stdinReader = new StreamReader(stdin, Console.InputEncoding); + _stdoutWriter = new StreamWriter(stdout, Console.OutputEncoding) {AutoFlush = true}; + } + + public static PluginHandler Create(Stream stdin, Stream stdout) + => new PluginHandler(stdin, stdout); + + internal async Task HandleNextRequestAsync( + Action> pluginRes + ) + { + var json = await _stdinReader.ReadLineAsync(); + + if (string.IsNullOrEmpty(json?.Trim())) return false; + + var request = new JsonRpcRequest(json); + + if (request.IsValid != true) return false; + + var responseHandler = new PluginResponse(_stdoutWriter, _plugin); + + pluginRes(request, responseHandler); + + return await responseHandler.RespondAsync(); + } + + public void Dispose() + { + _stdinReader?.Dispose(); + _stdoutWriter?.Dispose(); + } + } +} diff --git a/src/Nu.Plugin/PluginResponse.cs b/src/Nu.Plugin/PluginResponse.cs new file mode 100644 index 0000000..e190a81 --- /dev/null +++ b/src/Nu.Plugin/PluginResponse.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Nu.Plugin.Interfaces; + +namespace Nu.Plugin +{ + internal class PluginResponse where TPluginType : new() + { + private readonly TaskCompletionSource _tcs = new TaskCompletionSource(); + private readonly StreamWriter _writer; + private readonly TPluginType _plugin; + + public PluginResponse(StreamWriter writer, TPluginType plugin) + { + _plugin = plugin; + _writer = writer; + } + + public void Config(Signature signature) + { + Respond(JsonRpcResponse.Ok(signature)); + + Done(); + } + + public void Sink(IEnumerable requestParams) + { + if (_plugin is INuPluginSink sinkPlugin) + { + sinkPlugin.Sink(requestParams); + } + + Done(); + } + + public void BeginFilter() + { + if (_plugin is INuPluginFilter filterPlugin) + { + Respond(JsonRpcResponse.Ok(filterPlugin.BeginFilter())); + } + + Continue(); + } + + public void Filter(JsonRpcValue requestParams) + { + if (_plugin is INuPluginFilter filterPlugin) + { + Respond(JsonRpcResponse.Ok(filterPlugin.Filter(requestParams))); + } + + Continue(); + } + + public void EndFilter() + { + if (_plugin is INuPluginFilter filterPlugin) + { + Respond(JsonRpcResponse.Ok(filterPlugin.EndFilter())); + } + + Done(); + } + + public void Quit() => Done(); + + private void Respond(JsonRpcResponse rpcResponse) => + _writer.WriteLine(JsonSerializer.Serialize(rpcResponse)); + + private void Continue() => _tcs.SetResult(true); + + private void Done() => _tcs.SetResult(false); + + public Task RespondAsync() => _tcs.Task; + } +} diff --git a/src/Nu.Plugin/PositionalType.cs b/src/Nu.Plugin/PositionalType.cs new file mode 100644 index 0000000..3ce2234 --- /dev/null +++ b/src/Nu.Plugin/PositionalType.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Nu.Plugin +{ + public interface IPositionalType { } + + public class MandatoryPostionalType : IPositionalType + { + public MandatoryPostionalType(string name) : this(name, SyntaxShape.Any) { } + public MandatoryPostionalType(string name, SyntaxShape syntax) => Mandatory = new string[] { name, syntax.Shape }; + + [JsonPropertyName("Mandatory")] + public string[] Mandatory { get; } + } + + public class OptionalPostionalType : IPositionalType + { + public OptionalPostionalType(string name) : this(name, SyntaxShape.Any) { } + public OptionalPostionalType(string name, SyntaxShape syntax) => Optional = new string[] { name, syntax.Shape }; + + [JsonPropertyName("Optional")] + public string[] Optional { get; } + } +} diff --git a/src/Nu.Plugin/Signature.cs b/src/Nu.Plugin/Signature.cs index 89dddee..a41d47e 100644 --- a/src/Nu.Plugin/Signature.cs +++ b/src/Nu.Plugin/Signature.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; namespace Nu.Plugin @@ -7,13 +9,24 @@ internal class Signature { private Signature() { } - private Signature(string name, string usage, bool isFilter, int[] positional, object named) + private Signature( + string name, + string usage, + bool isFilter, + object[][] positional, + string[] restPositional, + IReadOnlyDictionary named, + object yields, object input + ) { IsFilter = isFilter; Name = name; Description = usage; Named = named; Positional = positional; + RestPositional = restPositional; + Yields = yields; + Input = input; } public static Signature Create() => new Signature(); @@ -25,45 +38,193 @@ private Signature(string name, string usage, bool isFilter, int[] positional, ob public string Description { get; } [JsonPropertyName("positional")] - public int[] Positional { get; } = Array.Empty(); + public object[][] Positional { get; } = Array.Empty(); [JsonPropertyName("rest_positional")] - public int[] RestPositional { get; } = null; + public string[] RestPositional { get; } = null; [JsonPropertyName("named")] - public object Named { get; } = new { }; + public IReadOnlyDictionary Named { get; } = + new Dictionary + {{"help", new object[] {new NamedTypeSwitch('h'), "Display this help message"}}}; [JsonPropertyName("yields")] - public object Yields { get; } = null; + public object Yields { get; } [JsonPropertyName("input")] - public object Input { get; } = null; + public object Input { get; } [JsonPropertyName("is_filter")] - public bool IsFilter { get; } = false; + public bool IsFilter { get; } public Signature WithName(string name) => new Signature( name, - this.Description, - this.IsFilter, - this.Positional, - this.Named + Description, + IsFilter, + Positional, + RestPositional, + Named, + Yields, + Input ); public Signature WithDescription(string description) => new Signature( - this.Name, + Name, description, - this.IsFilter, - this.Positional, - this.Named + IsFilter, + Positional, + RestPositional, + Named, + Yields, + Input ); public Signature WithIsFilter(bool isFilter) => new Signature( - this.Name, - this.Description, + Name, + Description, isFilter, - this.Positional, - this.Named + Positional, + RestPositional, + Named, + Yields, + Input ); + + public Signature AddRequiredPositional(SyntaxShape syntaxShape, string name, string description) + { + var positionalArgument = new object[] + { + new MandatoryPostionalType(name, syntaxShape), + description + }; + + var newPositionalArguments = new object[Positional.Length + 1][]; + if (Positional.Length > 0) + { + Positional.CopyTo(newPositionalArguments, 0); + } + + newPositionalArguments[Positional.Length] = positionalArgument; + + return new Signature( + Name, + Description, + IsFilter, + newPositionalArguments, + RestPositional, + Named, + Yields, + Input + ); + } + + public Signature AddOptionalPositional(SyntaxShape syntaxShape, string name, string description) + { + var positionalArgument = new object[] + { + new OptionalPostionalType(name, syntaxShape), + description + }; + + var newPositionalArguments = new object[Positional.Length + 1][]; + if (Positional.Length > 0) + { + Positional.CopyTo(newPositionalArguments, 0); + } + + newPositionalArguments[Positional.Length] = positionalArgument; + + return new Signature( + Name, + Description, + IsFilter, + newPositionalArguments, + RestPositional, + Named, + Yields, + Input + ); + } + + public Signature AddRestPositionalArguments(string description, SyntaxShape syntaxShape = null) + { + var newRestPositional = new string[] + { + (syntaxShape ?? SyntaxShape.Any).Shape, + description + }; + + return new Signature( + Name, + Description, + IsFilter, + Positional, + newRestPositional, + Named, + Yields, + Input + ); + } + + public Signature AddSwitch(string name, string description, char? flag = null) + { + if (!flag.HasValue) + { + flag = name.FirstOrDefault(); + } + + var namedTypeSwitch = new NamedTypeSwitch(flag.Value); + var named = Named.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + named.Add(name, new object[] { namedTypeSwitch, description }); + + return new Signature( + Name, + Description, + IsFilter, + Positional, + RestPositional, + named, + Yields, + Input + ); + } + + public Signature AddOptionalNamed(SyntaxShape syntaxShape, string name, string description, char? flag = null) + { + var flagValue = new string(new char[] { flag ?? name.FirstOrDefault() }); + var namedTypeOptional = new NamedTypeOptional(flagValue, syntaxShape); + var named = Named.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + named.Add(name, new object[] { namedTypeOptional, description }); + + return new Signature( + Name, + Description, + IsFilter, + Positional, + RestPositional, + named, + Yields, + Input + ); + } + + public Signature AddRequiredNamed(SyntaxShape syntaxShape, string name, string description, char? flag = null) + { + var flagValue = new string(new char[] { flag ?? name.FirstOrDefault() }); + var namedTypeMandatory = new NamedTypeMandatory(flagValue, syntaxShape); + var named = Named.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + named.Add(name, new object[] { namedTypeMandatory, description }); + + return new Signature( + Name, + Description, + IsFilter, + Positional, + RestPositional, + named, + Yields, + Input + ); + } } -} \ No newline at end of file +} diff --git a/src/Nu.Plugin/SyntaxShape.cs b/src/Nu.Plugin/SyntaxShape.cs new file mode 100644 index 0000000..57228d0 --- /dev/null +++ b/src/Nu.Plugin/SyntaxShape.cs @@ -0,0 +1,39 @@ +namespace Nu.Plugin +{ + public class SyntaxShape + { + private SyntaxShape(string shape) => Shape = shape; + + public string Shape { get; } + + /// Any syntactic form is allowed + public static SyntaxShape Any => new SyntaxShape(nameof(Any)); + + /// Strings and string-like bare words are allowed + public static SyntaxShape String => new SyntaxShape(nameof(String)); + + /// Values that can be the right hand side of a '.' + public static SyntaxShape Member => new SyntaxShape(nameof(Member)); + + /// A dotted path to navigate the table + public static SyntaxShape ColumnPath => new SyntaxShape(nameof(ColumnPath)); + + /// Only a numeric (integer or decimal) value is allowed + public static SyntaxShape Number => new SyntaxShape(nameof(Number)); + + /// A range is allowed (eg, `1..3`) + public static SyntaxShape Range => new SyntaxShape(nameof(Range)); + + /// Only an integer value is allowed + public static SyntaxShape Int => new SyntaxShape(nameof(Int)); + + /// A filepath is allowed + public static SyntaxShape Path => new SyntaxShape(nameof(Path)); + + /// A glob pattern is allowed, eg `foo*` + public static SyntaxShape Pattern => new SyntaxShape(nameof(Pattern)); + + /// A block is allowed, eg `{start this thing}` + public static SyntaxShape Block => new SyntaxShape(nameof(Block)); + } +} diff --git a/tests/Nu.Plugin.Tests/Nu.Plugin.Tests.csproj b/tests/Nu.Plugin.Tests/Nu.Plugin.Tests.csproj new file mode 100644 index 0000000..312ac7a --- /dev/null +++ b/tests/Nu.Plugin.Tests/Nu.Plugin.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/tests/Nu.Plugin.Tests/NuPluginTests.cs b/tests/Nu.Plugin.Tests/NuPluginTests.cs new file mode 100644 index 0000000..a5098fd --- /dev/null +++ b/tests/Nu.Plugin.Tests/NuPluginTests.cs @@ -0,0 +1,29 @@ +using Xunit; + +namespace Nu.Plugin.Tests +{ + public class NuPluginTests + { + private readonly NuPlugin _nuPlugin = NuPlugin.Build("name"); + + [Fact] + public void NuPlugin_Build_Returns_NuPlugin() + { + // Arrange & Act + var nuPlugin = NuPlugin.Build("name"); + + // Assert + Assert.NotNull(nuPlugin); + } + + [Fact] + public void NuPlugin_Description_Returns_NuPlugin() + { + // Arrange & Act + var nuPlugin = _nuPlugin.Description("description"); + + // Assert + Assert.NotNull(nuPlugin); + } + } +} diff --git a/tests/Nu.Plugin.Tests/SignatureTests.cs b/tests/Nu.Plugin.Tests/SignatureTests.cs new file mode 100644 index 0000000..87d9d40 --- /dev/null +++ b/tests/Nu.Plugin.Tests/SignatureTests.cs @@ -0,0 +1,147 @@ +using Xunit; + +namespace Nu.Plugin.Tests +{ + public class SignatureTests + { + private readonly Signature _signature = Signature.Create(); + + [Fact] + public void Signature_Create_Returns_New_Object() + { + // Arrange & Act & Assert + Assert.NotNull(_signature); + } + + [Fact] + public void Signature_WithDescription_Returns_New_Object() + { + // Arrange + var expectedValue = nameof(Signature.Description); + + // Act + var newSignature = _signature.WithDescription(expectedValue); + + // Assert + Assert.Equal(newSignature.Description, expectedValue); + Assert.NotEqual(_signature, newSignature); + Assert.NotEqual(_signature.Description, newSignature.Description); + } + + [Fact] + public void Signature_WithName_Returns_New_Object() + { + // Arrange + var expectedValue = nameof(Signature.Name); + + // Act + var newSignature = _signature.WithName(expectedValue); + + // Assert + Assert.Equal(newSignature.Name, expectedValue); + Assert.NotEqual(_signature, newSignature); + Assert.NotEqual(_signature.Name, newSignature.Name); + } + + [Fact] + public void Signature_WithIsFilter_Returns_New_Object() + { + // Arrange + var expectedValue = true; + + // Act + var newSignature = _signature.WithIsFilter(expectedValue); + + // Assert + Assert.Equal(newSignature.IsFilter, expectedValue); + Assert.NotEqual(_signature, newSignature); + Assert.NotEqual(_signature.IsFilter, newSignature.IsFilter); + } + + [Fact] + public void Signature_AddSwitch_Returns_New_Object() + { + // Arrange + var nameSwitch = "switch"; + var nameSwitchDescription = "switch description"; + + // Act + var newSignature = _signature.AddSwitch(nameSwitch, nameSwitchDescription); + + // Assert + Assert.True(newSignature.Named.ContainsKey(nameSwitch)); + Assert.Equal(2, newSignature.Named[nameSwitch].Length); + Assert.Equal(typeof(NamedTypeSwitch), newSignature.Named[nameSwitch][0].GetType()); + Assert.Equal(nameSwitchDescription, newSignature.Named[nameSwitch][1]); + } + + [Fact] + public void Signature_AddOptionalNamed_Returns_New_Object() + { + // Arrange + var nameOptional = "optional"; + var nameOptionalDescription = "optional description"; + var nameOptionalShape = SyntaxShape.Int; + + // Act + var newSignature = _signature.AddOptionalNamed(nameOptionalShape, nameOptional, nameOptionalDescription); + + // Assert + Assert.True(newSignature.Named.ContainsKey(nameOptional)); + + var optionalNamedParameter = newSignature.Named[nameOptional]; + Assert.Equal(2, optionalNamedParameter.Length); + Assert.Equal(nameOptionalDescription, optionalNamedParameter[1]); + + var actualOptionalObj = optionalNamedParameter[0] as NamedTypeOptional; + Assert.NotNull(actualOptionalObj); + Assert.Equal("o", actualOptionalObj.Optional[0]); + Assert.Equal(nameOptionalShape.Shape, actualOptionalObj.Optional[1]); + } + + [Fact] + public void Signature_AddRequiredNamed_Returns_New_Object() + { + // Arrange + var nameOptional = "required"; + var nameOptionalDescription = "required description"; + var nameOptionalShape = SyntaxShape.Path; + + // Act + var newSignature = _signature.AddRequiredNamed(nameOptionalShape, nameOptional, nameOptionalDescription); + + // Assert + Assert.True(newSignature.Named.ContainsKey(nameOptional)); + + var optionalNamedParameter = newSignature.Named[nameOptional]; + Assert.Equal(2, optionalNamedParameter.Length); + Assert.Equal(nameOptionalDescription, optionalNamedParameter[1]); + + var actualOptionalObj = optionalNamedParameter[0] as NamedTypeMandatory; + Assert.NotNull(actualOptionalObj); + Assert.Equal("r", actualOptionalObj.Mandatory[0]); + Assert.Equal(nameOptionalShape.Shape, actualOptionalObj.Mandatory[1]); + } + + [Fact] + public void Signature_Create_Returns_Default_Values() + { + // Arrange + var expectedDescriptionValue = nameof(Signature.Description); + var expectedNameValue = nameof(Signature.Name); + var expectedIsFilterValue = true; + + // Act + var newSignature = _signature + .WithDescription(expectedDescriptionValue) + .WithName(expectedNameValue) + .WithIsFilter(expectedIsFilterValue); + + // Assert + Assert.NotEqual(_signature, newSignature); + Assert.Equal(newSignature.Description, expectedDescriptionValue); + Assert.Equal(newSignature.Name, expectedNameValue); + Assert.Equal(newSignature.IsFilter, expectedIsFilterValue); + } + } +}