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 ef899c1..13e493a 100644 --- a/samples/Nu.Plugin.Len/Program.cs +++ b/samples/Nu.Plugin.Len/Program.cs @@ -1,31 +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(string[] args) => await NuPlugin.Create() - .Name("len") - .Usage("Return the length of a string") - .IsFilter() - .RunAsync(); - - 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(); + private static async Task Main() => await NuPlugin + .Build("len") + .Description("Return the length of a string") + .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/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/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/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/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 new file mode 100644 index 0000000..ae3886b --- /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); + } +} 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/JsonRpcRequest.cs b/src/Nu.Plugin/JsonRpc/JsonRpcRequest.cs index e317a22..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) { @@ -38,9 +38,16 @@ 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; } 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 997f997..146a148 100644 --- a/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs +++ b/src/Nu.Plugin/JsonRpc/JsonRpcResponse.cs @@ -1,18 +1,26 @@ -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 { - internal abstract class JsonRpcResponse + 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"; [JsonPropertyName("params")] - public abstract object Params { get; } + public object Params { get; } + + 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/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/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.cs b/src/Nu.Plugin/NuPlugin.cs index e259920..0488097 100644 --- a/src/Nu.Plugin/NuPlugin.cs +++ b/src/Nu.Plugin/NuPlugin.cs @@ -1,108 +1,141 @@ 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 : 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) + private NuPlugin(Stream stdin, Stream stdout, string name) { _stdout = stdout; _stdin = stdin; + _signature = _signature.WithName(name); } - public async Task RunAsync() + public static NuPlugin Build(string name) { - 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; } + var stdin = Console.OpenStandardInput(); + var stdout = Console.OpenStandardOutput(); - if (request.Method == "config") - { - OkResponse(_configuration); - break; - } - else if (_configuration.IsFilter && request.Method == "begin_filter") - { - OkResponse(_filter.BeginFilter()); - } - else if (request.Method == "filter") - { - var requestParams = request.GetParams(); + return new NuPlugin(stdin, stdout, name); + } - RpcValueResponse(_filter.Filter(requestParams)); - } - else if (request.Method == "end_filter") - { - OkResponse(_filter.EndFilter()); - break; - } - else - { - break; - } - } - } + public NuPlugin Description(string description) + { + _signature = _signature.WithDescription(description); + return this; } - private void OkResponse(object response) => Response(new JsonRpcOkResponse(response)); + public NuPlugin Required(SyntaxShape syntaxShape, string name, string description) + { + _signature = _signature.AddRequiredPositional(syntaxShape, name, description); + return this; + } - private void RpcValueResponse(JsonRpcParams rpcParams) => Response(new JsonRpcValueResponse(rpcParams)); + public NuPlugin Optional(SyntaxShape syntaxShape, string name, string description) + { + _signature = _signature.AddOptionalPositional(syntaxShape, name, description); + return this; + } - private void Response(JsonRpcResponse response) + public NuPlugin Switch(string name, string description, char? flag = null) { - var serializedResponse = JsonSerializer.Serialize(response); + _signature = _signature.AddSwitch(name, description, flag); + return this; + } - _standardOutputWriter.WriteLine(serializedResponse); + public NuPlugin Named(SyntaxShape syntaxShape, string name, string description, char? flag = null) + { + _signature = _signature.AddOptionalNamed(syntaxShape, name, description, flag); + return this; } - public static INuPluginBuilder Create(Stream stdin, Stream stdout) => new NuPlugin(stdin, stdout); + public NuPlugin RequiredNamed(SyntaxShape syntaxShape, string name, string description, char? flag = null) + { + _signature = _signature.AddRequiredNamed(syntaxShape, name, description, flag); + return this; + } - public static INuPluginBuilder Create() + public NuPlugin Rest(string description, SyntaxShape syntaxShape = null) { - var stdin = Console.OpenStandardInput(); - var stdout = Console.OpenStandardOutput(); + _signature = _signature.AddRestPositionalArguments(description, syntaxShape); + return this; + } - return new NuPlugin(stdin, stdout); + public NuPlugin Yields() + { + throw new NotImplementedException("To be implemented in the future"); } - public INuPluginBuilder Name(string name) + public NuPlugin Input() { - _configuration = _configuration.WithName(name); - return this; + throw new NotImplementedException("To be implemented in the future"); } - public INuPluginBuilder Usage(string usage) + public async Task SinkAsync() where TSinkPlugin : INuPluginSink, new() { - _configuration = _configuration.WithUsage(usage); - return this; + await CommandHandler( + (req, res) => + { + switch (req.Method) + { + case "config": + res.Config(_signature); + break; + case "sink": + { + var requestParams = req.GetParams>(); + res.Sink(requestParams); + break; + } + default: + res.Quit(); + break; + } + } + ); } - public INuPluginBuilder IsFilter() where T : INuPluginFilter, new() + public async Task FilterAsync() where TFilterPlugin : INuPluginFilter, new() { - _configuration = _configuration.WithIsFilter(true); - _filter = new T(); + await CommandHandler( + (req, res) => + { + 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; + } + } + ); + } - return this; + 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 new file mode 100644 index 0000000..a41d47e --- /dev/null +++ b/src/Nu.Plugin/Signature.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Nu.Plugin +{ + internal class Signature + { + private Signature() { } + + 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(); + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("usage")] + public string Description { get; } + + [JsonPropertyName("positional")] + public object[][] Positional { get; } = Array.Empty(); + + [JsonPropertyName("rest_positional")] + public string[] RestPositional { get; } = null; + + [JsonPropertyName("named")] + public IReadOnlyDictionary Named { get; } = + new Dictionary + {{"help", new object[] {new NamedTypeSwitch('h'), "Display this help message"}}}; + + [JsonPropertyName("yields")] + public object Yields { get; } + + [JsonPropertyName("input")] + public object Input { get; } + + [JsonPropertyName("is_filter")] + public bool IsFilter { get; } + + public Signature WithName(string name) => new Signature( + name, + Description, + IsFilter, + Positional, + RestPositional, + Named, + Yields, + Input + ); + + public Signature WithDescription(string description) => new Signature( + Name, + description, + IsFilter, + Positional, + RestPositional, + Named, + Yields, + Input + ); + + public Signature WithIsFilter(bool isFilter) => new Signature( + Name, + Description, + isFilter, + 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 + ); + } + } +} 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); + } + } +}