| | | 1 | | using Microsoft.CodeAnalysis; |
| | | 2 | | |
| | | 3 | | namespace NexusLabs.Needlr.Generators; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Helper for discovering Options attributes from Roslyn symbols. |
| | | 7 | | /// </summary> |
| | | 8 | | internal static class OptionsAttributeHelper |
| | | 9 | | { |
| | | 10 | | private const string OptionsAttributeName = "OptionsAttribute"; |
| | | 11 | | private const string OptionsAttributeFullName = "NexusLabs.Needlr.OptionsAttribute"; |
| | | 12 | | |
| | | 13 | | /// <summary> |
| | | 14 | | /// Information extracted from an [Options] attribute. |
| | | 15 | | /// </summary> |
| | | 16 | | public readonly struct OptionsAttributeInfo |
| | | 17 | | { |
| | | 18 | | public OptionsAttributeInfo(string? sectionName, string? name, bool validateOnStart, string? validateMethod = nu |
| | | 19 | | { |
| | 173 | 20 | | SectionName = sectionName; |
| | 173 | 21 | | Name = name; |
| | 173 | 22 | | ValidateOnStart = validateOnStart; |
| | 173 | 23 | | ValidateMethod = validateMethod; |
| | 173 | 24 | | ValidatorType = validatorType; |
| | 173 | 25 | | } |
| | | 26 | | |
| | | 27 | | /// <summary>Explicit section name from attribute, or null to infer from class name.</summary> |
| | 173 | 28 | | public string? SectionName { get; } |
| | | 29 | | |
| | | 30 | | /// <summary>Named options name (e.g., "Primary"), or null for default options.</summary> |
| | 173 | 31 | | public string? Name { get; } |
| | | 32 | | |
| | | 33 | | /// <summary>Whether to validate options on startup.</summary> |
| | 173 | 34 | | public bool ValidateOnStart { get; } |
| | | 35 | | |
| | | 36 | | /// <summary>Custom validation method name, or null to use convention ("Validate").</summary> |
| | 346 | 37 | | public string? ValidateMethod { get; } |
| | | 38 | | |
| | | 39 | | /// <summary>External validator type, or null to use the options class itself.</summary> |
| | 173 | 40 | | public INamedTypeSymbol? ValidatorType { get; } |
| | | 41 | | } |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Checks if a type has the [Options] attribute. |
| | | 45 | | /// </summary> |
| | | 46 | | /// <param name="typeSymbol">The type symbol to check.</param> |
| | | 47 | | /// <returns>True if the type has [Options]; otherwise, false.</returns> |
| | | 48 | | public static bool HasOptionsAttribute(INamedTypeSymbol typeSymbol) |
| | | 49 | | { |
| | 6828099 | 50 | | foreach (var attribute in typeSymbol.GetAttributes()) |
| | | 51 | | { |
| | 1993853 | 52 | | var attributeClass = attribute.AttributeClass; |
| | 1993853 | 53 | | if (attributeClass == null) |
| | | 54 | | continue; |
| | | 55 | | |
| | 1993853 | 56 | | var name = attributeClass.Name; |
| | 1993853 | 57 | | if (name == OptionsAttributeName) |
| | 167 | 58 | | return true; |
| | | 59 | | |
| | 1993686 | 60 | | var fullName = attributeClass.ToDisplayString(); |
| | 1993686 | 61 | | if (fullName == OptionsAttributeFullName) |
| | 0 | 62 | | return true; |
| | | 63 | | } |
| | | 64 | | |
| | 1420113 | 65 | | return false; |
| | | 66 | | } |
| | | 67 | | |
| | | 68 | | /// <summary> |
| | | 69 | | /// Gets all [Options] attribute data from a type. |
| | | 70 | | /// </summary> |
| | | 71 | | /// <param name="typeSymbol">The type symbol to check.</param> |
| | | 72 | | /// <returns>A list of options attribute info for each [Options] on the type.</returns> |
| | | 73 | | public static IReadOnlyList<OptionsAttributeInfo> GetOptionsAttributes(INamedTypeSymbol typeSymbol) |
| | | 74 | | { |
| | 167 | 75 | | var result = new List<OptionsAttributeInfo>(); |
| | | 76 | | |
| | 680 | 77 | | foreach (var attribute in typeSymbol.GetAttributes()) |
| | | 78 | | { |
| | 173 | 79 | | var attributeClass = attribute.AttributeClass; |
| | 173 | 80 | | if (attributeClass == null) |
| | | 81 | | continue; |
| | | 82 | | |
| | 173 | 83 | | var name = attributeClass.Name; |
| | 173 | 84 | | var fullName = attributeClass.ToDisplayString(); |
| | | 85 | | |
| | 173 | 86 | | if (name != OptionsAttributeName && fullName != OptionsAttributeFullName) |
| | | 87 | | continue; |
| | | 88 | | |
| | | 89 | | // Extract constructor argument (optional section name) |
| | 173 | 90 | | string? sectionName = null; |
| | 173 | 91 | | if (attribute.ConstructorArguments.Length > 0 && |
| | 173 | 92 | | attribute.ConstructorArguments[0].Value is string section) |
| | | 93 | | { |
| | 86 | 94 | | sectionName = section; |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | // Extract named arguments |
| | 173 | 98 | | string? optionsName = null; |
| | 173 | 99 | | bool validateOnStart = false; |
| | 173 | 100 | | string? validateMethod = null; |
| | 173 | 101 | | INamedTypeSymbol? validatorType = null; |
| | | 102 | | |
| | 502 | 103 | | foreach (var namedArg in attribute.NamedArguments) |
| | | 104 | | { |
| | 78 | 105 | | if (namedArg.Key == "Name" && namedArg.Value.Value is string n) |
| | | 106 | | { |
| | 22 | 107 | | optionsName = n; |
| | | 108 | | } |
| | 56 | 109 | | else if (namedArg.Key == "ValidateOnStart" && namedArg.Value.Value is bool v) |
| | | 110 | | { |
| | 46 | 111 | | validateOnStart = v; |
| | | 112 | | } |
| | 10 | 113 | | else if (namedArg.Key == "ValidateMethod" && namedArg.Value.Value is string vm) |
| | | 114 | | { |
| | 3 | 115 | | validateMethod = vm; |
| | | 116 | | } |
| | 7 | 117 | | else if (namedArg.Key == "Validator" && namedArg.Value.Value is INamedTypeSymbol vt) |
| | | 118 | | { |
| | 6 | 119 | | validatorType = vt; |
| | | 120 | | } |
| | | 121 | | } |
| | | 122 | | |
| | 173 | 123 | | result.Add(new OptionsAttributeInfo(sectionName, optionsName, validateOnStart, validateMethod, validatorType |
| | | 124 | | } |
| | | 125 | | |
| | 167 | 126 | | return result; |
| | | 127 | | } |
| | | 128 | | |
| | | 129 | | /// <summary> |
| | | 130 | | /// Finds a validation method on a type by convention or explicit name. |
| | | 131 | | /// Convention: method named "Validate" (or custom name via ValidateMethod property). |
| | | 132 | | /// </summary> |
| | | 133 | | /// <param name="typeSymbol">The type symbol to search.</param> |
| | | 134 | | /// <param name="methodName">The method name to look for (default: "Validate").</param> |
| | | 135 | | /// <returns>Validator method info, or null if no validator method found.</returns> |
| | | 136 | | public static OptionsValidatorMethodInfo? FindValidationMethod(INamedTypeSymbol typeSymbol, string methodName = "Val |
| | | 137 | | { |
| | | 138 | | // Look for method by name (convention-based) |
| | 2948 | 139 | | foreach (var member in typeSymbol.GetMembers()) |
| | | 140 | | { |
| | 1310 | 141 | | if (member is not IMethodSymbol method) |
| | | 142 | | continue; |
| | | 143 | | |
| | 800 | 144 | | if (method.Name != methodName) |
| | | 145 | | continue; |
| | | 146 | | |
| | | 147 | | // Check signature: should return IEnumerable<string> or IEnumerable<ValidationError> |
| | | 148 | | // Supported signatures: |
| | | 149 | | // 1. Instance method with no parameters: T.Validate() - for self-validation |
| | | 150 | | // 2. Static method with one parameter: static T.Validate(TOptions options) |
| | | 151 | | // 3. Instance method with one parameter: validator.Validate(TOptions options) - for external validators |
| | 18 | 152 | | if (method.Parameters.Length == 0 && !method.IsStatic) |
| | | 153 | | { |
| | | 154 | | // Instance method: T.Validate() - self-validation on options class |
| | 11 | 155 | | return new OptionsValidatorMethodInfo(method.Name, false); |
| | | 156 | | } |
| | | 157 | | |
| | 7 | 158 | | if (method.Parameters.Length == 1 && method.IsStatic) |
| | | 159 | | { |
| | | 160 | | // Static method: T.Validate(T options) - static validator |
| | 3 | 161 | | return new OptionsValidatorMethodInfo(method.Name, true); |
| | | 162 | | } |
| | | 163 | | |
| | 4 | 164 | | if (method.Parameters.Length == 1 && !method.IsStatic) |
| | | 165 | | { |
| | | 166 | | // Instance method with parameter: validator.Validate(T options) - external validator |
| | 4 | 167 | | return new OptionsValidatorMethodInfo(method.Name, false); |
| | | 168 | | } |
| | | 169 | | } |
| | | 170 | | |
| | 155 | 171 | | return null; |
| | | 172 | | } |
| | | 173 | | |
| | | 174 | | /// <summary> |
| | | 175 | | /// Information about a validation method. |
| | | 176 | | /// </summary> |
| | | 177 | | public readonly struct OptionsValidatorMethodInfo |
| | | 178 | | { |
| | | 179 | | public OptionsValidatorMethodInfo(string methodName, bool isStatic) |
| | | 180 | | { |
| | 18 | 181 | | MethodName = methodName; |
| | 18 | 182 | | IsStatic = isStatic; |
| | 18 | 183 | | } |
| | | 184 | | |
| | 18 | 185 | | public string MethodName { get; } |
| | 18 | 186 | | public bool IsStatic { get; } |
| | | 187 | | } |
| | | 188 | | } |