diff --git a/AutoSpectre.SourceGeneration.Tests/IncrementAutoSpectreGeneratorTests.cs b/AutoSpectre.SourceGeneration.Tests/IncrementAutoSpectreGeneratorTests.cs index 9306e2f..6542d7d 100644 --- a/AutoSpectre.SourceGeneration.Tests/IncrementAutoSpectreGeneratorTests.cs +++ b/AutoSpectre.SourceGeneration.Tests/IncrementAutoSpectreGeneratorTests.cs @@ -226,7 +226,7 @@ namespace Test [AutoSpectreForm] public class TestForm { - [Ask(AskType = AskType.Selection, SelectionSource = nameof(ListOfOther))] + [Ask(AskType = AskType.Selection)] public List Other {get;set;} public string OtherConverter(OtherTest.OtherClass other) @@ -235,7 +235,7 @@ public string OtherConverter(OtherTest.OtherClass other) } - public List ListOfOther {get;set;} = new (); + public List OtherSource {get;set;} = new (); } } diff --git a/AutoSpectre.SourceGeneration/AskBuilder.cs b/AutoSpectre.SourceGeneration/AskBuilder.cs deleted file mode 100644 index bba555b..0000000 --- a/AutoSpectre.SourceGeneration/AskBuilder.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text; - -namespace AutoSpectre.SourceGeneration; - -public abstract class AskBuilder -{ - public abstract void Build(StringBuilder builder); -} \ No newline at end of file diff --git a/AutoSpectre.SourceGeneration/AttributeCode.cs b/AutoSpectre.SourceGeneration/AttributeCode.cs deleted file mode 100644 index f0e1467..0000000 --- a/AutoSpectre.SourceGeneration/AttributeCode.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace AutoSpectre.SourceGeneration; - -public static class AttributeCode -{ - //public static readonly string AutoSpectreFromAttribute = """ - // using System; - - // namespace AutoSpectre; - - // /// - // /// Marker interface. Apply this to a class and a factory will - // /// - // public class AutoSpectreForm : Attribute - // { - - // } - // """; - - //public static readonly string AskAttribute = """ - // using System; - - // namespace AutoSpectre - // { - // public class AskAttribute : Attribute - // { - // public AskAttribute() - // { - - // } - - // [Obsolete()] - // public AskAttribute(string? title = null) - // { - - // } - - // //public AskAttribute(string? title = null, AskType askType = AutoSpectre.AskType.Normal, string? selectionSource = null) - // //{ - // // Title = title; - // // AskType = askType; - // // SelectionSource = selectionSource; - // //} - - // /// - // /// The title displayed. If nothing is defined a text including property name is displayed - // /// - // public string? Title { get; set; } - - // /// - // /// The type used to ask with - // /// - // public AskType AskType { get; set; } - // public string? SelectionSource { get; set; } - // } - - // public enum AskType - // { - // /// - // /// Default. - // /// - // Normal, - // /// - // /// Presents a selection dialog - // /// - // Selection - // } - // } - - // """; - - -} \ No newline at end of file diff --git a/AutoSpectre.SourceGeneration/AutoSpectre.SourceGeneration.csproj b/AutoSpectre.SourceGeneration/AutoSpectre.SourceGeneration.csproj index 7cc095f..cbeae62 100644 --- a/AutoSpectre.SourceGeneration/AutoSpectre.SourceGeneration.csproj +++ b/AutoSpectre.SourceGeneration/AutoSpectre.SourceGeneration.csproj @@ -6,7 +6,7 @@ enable true true - 0.3.4.0 + 0.3.5.0 @@ -20,7 +20,7 @@ https://github.com/jeppevammenkristensen/auto-spectre https://github.com/jeppevammenkristensen/auto-spectre C#;Source Generator - Fixed issue with mapped typed used in List in other mapped type + Add conventions for Converter and SelectionSource diff --git a/AutoSpectre.SourceGeneration/PropertyContextBuilderOperation.cs b/AutoSpectre.SourceGeneration/PropertyContextBuilderOperation.cs index b208ac6..b21a2f1 100644 --- a/AutoSpectre.SourceGeneration/PropertyContextBuilderOperation.cs +++ b/AutoSpectre.SourceGeneration/PropertyContextBuilderOperation.cs @@ -77,17 +77,13 @@ public List GetPropertyContexts() foreach (var (property, attributeData) in PropertyCandidates) { - var (nullable, originalType) = property.Type.GetTypeWithNullableInformation(); - var (enumerable, underlying) = property.Type.IsEnumerableOfTypeButNotString(); - - var propertyEvaluationContext = - new SinglePropertyEvaluationContext(property: property,isNullable: nullable, type: originalType, isEnumerable: enumerable, underlyingType: underlying); + var propertyContext = GenerateSinglePropertyEvaluationContext(property); if (attributeData.AskType == AskTypeCopy.Normal) { - if (!propertyEvaluationContext.IsEnumerable) + if (!propertyContext.IsEnumerable) { - if (GetNormalPromptBuildContext(attributeData.Title, propertyEvaluationContext) is + if (GetNormalPromptBuildContext(attributeData.Title, propertyContext) is { } promptBuildContext) { propertyContexts.Add(new(property.Name, property, @@ -96,67 +92,78 @@ public List GetPropertyContexts() } else { - if (GetNormalPromptBuildContext(attributeData.Title, propertyEvaluationContext) is + if (GetNormalPromptBuildContext(attributeData.Title, propertyContext) is { } promptBuildContext) { - propertyContexts.Add(new(property.Name, property, new MultiAddBuildContext(propertyEvaluationContext.Type, propertyEvaluationContext.UnderlyingType, types, promptBuildContext))); + propertyContexts.Add(new(property.Name, property, new MultiAddBuildContext(propertyContext.Type, propertyContext.UnderlyingType, types, promptBuildContext))); } } } if (attributeData.AskType == AskTypeCopy.Selection) { - EvaluateSelectionConverter(attributeData, propertyEvaluationContext); - - if (attributeData.SelectionSource is { }) - { - var match = TargetType - .GetMembers() - .Where(x => x.Name == attributeData.SelectionSource) - .FirstOrDefault(x => x is IMethodSymbol - { - Parameters.Length: 0 - } or IPropertySymbol {GetMethod: { }}); + EvaluateSelectionConverter(attributeData, propertyContext); + + var selectionSource = attributeData.SelectionSource ?? $"{propertyContext.Property.Name}Source"; - if (match is { }) + var match = TargetType + .GetMembers() + .Where(x => x.Name == selectionSource) + .FirstOrDefault(x => x is IMethodSymbol { - SelectionPromptSelectionType selectionType = match switch - { - IMethodSymbol => SelectionPromptSelectionType.Method, - IPropertySymbol => SelectionPromptSelectionType.Property, - _ => throw new NotSupportedException(), - }; - if (!propertyEvaluationContext.IsEnumerable) - { - propertyContexts.Add(new(property.Name, property, - new SelectionPromptBuildContext(attributeData.Title, propertyEvaluationContext, - attributeData.SelectionSource, selectionType))); - } - else - { - propertyContexts.Add(new(property.Name, property, - new MultiSelectionBuildContext(title: attributeData.Title, - propertyEvaluationContext, - selectionTypeName: attributeData.SelectionSource, - selectionType: selectionType, types))); - } + Parameters.Length: 0 + } or IPropertySymbol { GetMethod: { } }); + + if (match is { }) + { + SelectionPromptSelectionType selectionType = match switch + { + IMethodSymbol => SelectionPromptSelectionType.Method, + IPropertySymbol => SelectionPromptSelectionType.Property, + _ => throw new NotSupportedException(), + }; + if (!propertyContext.IsEnumerable) + { + propertyContexts.Add(new(property.Name, property, + new SelectionPromptBuildContext(attributeData.Title, propertyContext, + selectionSource, selectionType))); } - else + else { - ProductionContext.ReportDiagnostic(Diagnostic.Create( - new("AutoSpectre_JJK0005", - "Not a valid selection source", - $"The selectionsource {attributeData.SelectionSource} was not found on type", - "General", DiagnosticSeverity.Warning, true), - property.Locations.FirstOrDefault())); + propertyContexts.Add(new(property.Name, property, + new MultiSelectionBuildContext(title: attributeData.Title, + propertyContext, + selectionTypeName: selectionSource, + selectionType: selectionType, types))); } } + else + { + ProductionContext.ReportDiagnostic(Diagnostic.Create( + new("AutoSpectre_JJK0005", + "Not a valid selection source", + $"The selectionsource {attributeData.SelectionSource} was not found on type", + "General", DiagnosticSeverity.Warning, true), + property.Locations.FirstOrDefault())); + } + } } return propertyContexts; } + private static SinglePropertyEvaluationContext GenerateSinglePropertyEvaluationContext(IPropertySymbol property) + { + var (nullable, originalType) = property.Type.GetTypeWithNullableInformation(); + var (enumerable, underlying) = property.Type.IsEnumerableOfTypeButNotString(); + + var propertyEvaluationContext = + new SinglePropertyEvaluationContext(property: property, isNullable: nullable, type: originalType, + isEnumerable: enumerable, underlyingType: underlying); + return propertyEvaluationContext; + } + /// /// Evalutes the Converter set on the attributeData. If it's correct a valid converter is set on the context. /// if it is set but not valid a warning is reported. diff --git a/AutoSpectre/AskAttribute.cs b/AutoSpectre/AskAttribute.cs index f5ec03b..8365cbb 100644 --- a/AutoSpectre/AskAttribute.cs +++ b/AutoSpectre/AskAttribute.cs @@ -2,6 +2,13 @@ namespace AutoSpectre { + /// + /// Apply this attribute to a property to tell that a prompt should take place + /// + ///[Ask()] + /// public string Name {get;set;} + /// + /// public class AskAttribute : Attribute { public AskAttribute() @@ -14,32 +21,40 @@ public AskAttribute(string? title = null) { } - - //public AskAttribute(string? title = null, AskType askType = AutoSpectre.AskType.Normal, string? selectionSource = null) - //{ - // Title = title; - // AskType = askType; - // SelectionSource = selectionSource; - //} - + /// /// The title displayed. If nothing is defined a text including property name is displayed /// public string? Title { get; set; } /// - /// The type used to ask with + /// The type of prompting /// public AskType AskType { get; set; } + + /// + /// The source to get a list of items to present to the user. + /// Only relevant if the AskType is + /// The source pointed to must be either a property or method (with no input parameters) + /// that returns a list of the same type as defined on the property. + /// NOTE: If you define a source as {NameOfProperty}Source by convention that will be used + /// as source and you don't have to define a selection source + /// public string? SelectionSource { get; set; } + /// + /// The method that can create a string representation of the given property. The method must + /// take the type of the property as input parameter and return a string. + /// NOTE: If you define a converter method as {NameOfProperty}Converter you don't have to define + /// Converter + /// public string? Converter { get; set; } } public enum AskType { /// - /// Default. + /// Default. The user will input the values /// Normal, /// diff --git a/AutoSpectre/AutoSpectre.csproj b/AutoSpectre/AutoSpectre.csproj index b491c99..41dc20e 100644 --- a/AutoSpectre/AutoSpectre.csproj +++ b/AutoSpectre/AutoSpectre.csproj @@ -1,7 +1,7 @@  - 0.2.1.0 + 0.2.2.0 netstandard2.0 latest enable @@ -9,8 +9,12 @@ Classes used by the AutoSpectre.SourceGeneration package Jeppe Roi Kristensen https://github.com/jeppevammenkristensen/auto-spectre - Added Converter property to attribute + Added better summaries for classes + + bin\Release\AutoSpectre.xml + + diff --git a/AutoSpectre/AutoSpectreForm.cs b/AutoSpectre/AutoSpectreForm.cs index f884fbe..4f31aa4 100644 --- a/AutoSpectre/AutoSpectreForm.cs +++ b/AutoSpectre/AutoSpectreForm.cs @@ -3,7 +3,8 @@ namespace AutoSpectre; /// -/// Marker interface. Apply this to a class and a factory will +/// Marker interface. Apply this to a class and a factory will be generated +/// based on the properties in decorated with the /// public class AutoSpectreForm : Attribute { diff --git a/README.md b/README.md index 548fc6c..d14f680 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ [![Nuget](https://img.shields.io/nuget/v/AutoSpectre.SourceGeneration?style=flat-square)](https://www.nuget.org/packages/AutoSpectre.SourceGeneration) # Auto Spectre + Source generator project to generate classes that can be used in a console to prompt for values using Spectre.Console ## Short Guide -Decorate a class with the AutoSpectreForm attribute and then decorate the properties (must be settable) with AskAttribute. + +Decorate a class with the AutoSpectreForm attribute and then decorate the properties (must be settable) with AskAttribute. ### Example input @@ -37,9 +39,10 @@ Decorate a class with the AutoSpectreForm attribute and then decorate the proper } ``` -Behind the scenes this will generate an interface factory and implentation using `Spectre.Console` to prompt for the values. +Behind the scenes this will generate an interface factory and implentation using `Spectre.Console` to prompt for the values. + +### Example output -### Example output ### ```csharp public interface ISomeclassSpectreFactory { @@ -101,7 +104,7 @@ Behind the scenes this will generate an interface factory and implentation using } ``` -### How to call ### +### How to call ```csharp ISomeClassSpectreFactory factory = new SomeClassSpectreFactory(); @@ -115,7 +118,7 @@ factory.Get(someclass); ### Multiselect -if you use the AskType.Selection combined with a IEnumerable type the user will be presented with a multiselect. +if you use the AskType.Selection combined with a IEnumerable type the user will be presented with a multiselect. This property @@ -125,6 +128,7 @@ public string[] ArrayMultiSelect { get; set; } = Array.Empty(); ``` Will become + ```csharp destination.ArrayMultiSelect = AnsiConsole.Prompt(new MultiSelectionPrompt().Title("Enter [green]ArrayMultiSelect[/]").PageSize(10).AddChoices(destination.Items.ToArray())).ToArray(); ``` @@ -135,10 +139,11 @@ With the prompt below ### Strategy -The multiselection prompt always returns List of given type. In the above example (`List`) But AutoSpectre will attempt to adjust to the property type. +The multiselection prompt always returns List of given type. In the above example (`List`) But AutoSpectre will attempt to adjust to the property type. + * Array will result in a ToList() * HashSet will be initalized with new HashSet<> -* Immutable collection types will append ToImmutable{Type}() +* Immutable collection types will append ToImmutable{Type}() * Interfaces like `IList` `ICollection` `IEnumerable` `IReadOnlyCollection` `IReadOnlyList` have their values directly set as `List` inherit directly from them ### Example @@ -170,6 +175,7 @@ public class CollectionSample ``` #### Generated + ```csharp public interface ICollectionSampleSpectreFactory { @@ -224,6 +230,7 @@ public class ConverterForms ``` #### Generated + ```csharp public class ConverterFormsSpectreFactory : IConverterFormsSpectreFactory { @@ -237,3 +244,37 @@ public class ConverterFormsSpectreFactory : IConverterFormsSpectreFactory } ``` +## Conventions + +The following conventions come into play + +### SelectionSource + +You can leave out the SelectionSource in the Ask Attribute if you have a property or method that is named {NameOfProperty}Source and have the correct structure ( No input parameters and returns an enumerable of the type of the given property) + +#### Example + +```csharp +[Ask(AskType = AskType.Selection)] +public string Name { get; set; } + +public IEnumerable NameSource() +{ + yield return "John Doe"; + yield return "Jane Doe"; + yield return "John Smith"; +} +``` + +### Converter + +You can also leave out the Converter in the Ask Attribute if you have a method with the name {NameOfProperty}Converter and the correct structure (One parameter of the same type as the property and returning a string) + +#### Example + +```csharp +[Ask(AskType = AskType.Selection)] +public FullName Name { get; set; } + +public string NameConverter(FullName name) => $"{name.FirstName} {name.LastName}"; +```