diff --git a/.github/workflows/publish-experimental.yml b/.github/workflows/publish-experimental.yml new file mode 100644 index 0000000..f7f57eb --- /dev/null +++ b/.github/workflows/publish-experimental.yml @@ -0,0 +1,26 @@ +name: Publish Experimental +on: + workflow_dispatch: + +jobs: + publish: + env: + MainProject: 'GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.csproj' + BUILD_VER : '2.1' + name: build, pack & publish + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.0.x + - name: Setup NuGet + uses: nuget/setup-nuget@v1 + - name: Get Build Version + run: | + echo "BUILD_VERSION=${{ format('{0}.{1}', env.BUILD_VER, github.run_number ) }}-alpha" >> $GITHUB_ENV + - name: Build Main + run: dotnet pack $MainProject --configuration Release -p:Version=$BUILD_VERSION -p:PackageVersion=$BUILD_VERSION -p:NuGetVersion=$BUILD_VERSION -p:GeneratePackageOnBuild=false + - name: Publish + run: nuget push **\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_KEY}} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index a0244d1..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Publish to Nuget -on: - push: - branches: - - master # Default release branch - workflow_dispatch: - -jobs: - publish: - name: build, pack & publish - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.101 - - name: Publish - id: publish_nuget - uses: rohith/publish-nuget@v2 - with: - PROJECT_FILE_PATH: GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.csproj - VERSION_FILE_PATH: Directory.Build.props - VERSION_REGEX: ^\s*(.*)<\/Version>\s*$ - NUGET_KEY: ${{secrets.NUGET_KEY}} - INCLUDE_SYMBOLS: false diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 4c320dc..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,5 +0,0 @@ - - - 1.2.1 - - \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator.Console/GoLive.Generator.RazorPageRoute.Generator.Console.csproj b/GoLive.Generator.RazorPageRoute.Generator.Console/GoLive.Generator.RazorPageRoute.Generator.Console.csproj new file mode 100644 index 0000000..4a4482f --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator.Console/GoLive.Generator.RazorPageRoute.Generator.Console.csproj @@ -0,0 +1,26 @@ + + + + Exe + net9.0 + enable + enable + false + en + + + + x64 + + + + + + + + + + + + + diff --git a/GoLive.Generator.RazorPageRoute.Generator.Console/Program.cs b/GoLive.Generator.RazorPageRoute.Generator.Console/Program.cs new file mode 100644 index 0000000..2c17d2a --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator.Console/Program.cs @@ -0,0 +1,62 @@ +// See https://aka.ms/new-console-template for more information +using GoLive.Generator.RazorPageRoute.Generator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mono.Cecil; +using System.Linq; +using System.Linq.Expressions; + + +if (args.Length < 3) +{ + Console.WriteLine("Parameters required to run - "); + return; +} + +var settingsFile = args[0]; +var projectPath = args[1]; +var @namespace = args[2]; + +Console.WriteLine($"Running RPRG for settings: {settingsFile} in project path of {projectPath} with a namespace of {@namespace}"); + +var settings = PageRouteIncrementalExperimentalGenerator.LoadConfigFromFile(settingsFile, @namespace); +var routes = GetPageRoutes(projectPath).ToList(); + +IEnumerable GetPageRoutes(string projectPath) +{ + try + { + string dllFile; + dllFile = Scanner.GetDllPathFromProject(projectPath, out var assemblyResolver, [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet", "shared", "Microsoft.AspNetCore.App", "9.0.0")]); + using var assembly = AssemblyDefinition.ReadAssembly(dllFile, new ReaderParameters { AssemblyResolver = assemblyResolver }); + + return Scanner.ScanForPageRoutesIncremental(assembly, settings).DistinctBy(route => route.Route).ToList(); + } + catch (FileNotFoundException) + { + Console.WriteLine("refInt directory not found, scanning for C# files instead."); + return FileParseScanner.ParseCSharpFiles(projectPath); + } +} + +IEnumerable<(string MethodName, string InvokableName)> GetInvokeables(string projectPath) +{ + try + { + var dllFile = Scanner.GetDllPathFromProject(projectPath, out var assemblyResolver, [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet", "shared", "Microsoft.AspNetCore.App", "9.0.0")]); + + using var assembly = AssemblyDefinition.ReadAssembly(dllFile, new ReaderParameters { AssemblyResolver = assemblyResolver }); + + return Scanner.ScanForInvokables(assembly); + } + catch (FileNotFoundException e) + { + Console.WriteLine("refInt directory not found, source scanning not currently supported for invokables."); + + return new List<(string MethodName, string InvokableName)>();; + } +} + +PageRouteIncrementalExperimentalGenerator.GenerateOutput(default, settings, routes); +PageRouteIncrementalExperimentalGenerator.GenerateJSInvokable(settings, GetInvokeables(projectPath).ToList()); \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/AttributeContainer.cs b/GoLive.Generator.RazorPageRoute.Generator/AttributeContainer.cs new file mode 100644 index 0000000..3b6dd5b --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/AttributeContainer.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace GoLive.Generator.RazorPageRoute.Generator; + +public class AttributeContainer +{ + public string Name { get; set; } + public List Values { get; set; } = new(); +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Ext.cs b/GoLive.Generator.RazorPageRoute.Generator/Ext.cs new file mode 100644 index 0000000..bab7d0e --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Ext.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GoLive.Generator.RazorPageRoute.Generator; + +public static class Ext +{ + public static IEnumerable CustomDistinctBy(this IEnumerable items, Func property) + { + return items.GroupBy(property).Select(x => x.First()); + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/FileParseScanner.cs b/GoLive.Generator.RazorPageRoute.Generator/FileParseScanner.cs new file mode 100644 index 0000000..c2345c0 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/FileParseScanner.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace GoLive.Generator.RazorPageRoute.Generator; + +public static class FileParseScanner +{ + public static IEnumerable ParseCSharpFiles(string ProjectPath) + { + var rootPath = Path.Combine(ProjectPath, "obj//Debug//net9.0//RazorDeclaration//Pages"); + + var files = Directory.GetFiles(rootPath, "*.cs", SearchOption.AllDirectories); + + var attributes = RouteAttributeExtractor.ExtractRoutes(rootPath); + + if (attributes.Any()) + { + foreach (var extractedRoute in attributes) + { + foreach (var extractedRouteRoute in extractedRoute.Routes) + { + yield return new PageRoute(extractedRoute.ClassName, extractedRouteRoute, extractedRoute.QueryString); + } + } + } + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.csproj b/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.csproj index 568e4e1..5bbd482 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.csproj +++ b/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.csproj @@ -1,34 +1,51 @@ - - + + - netstandard2.0 - 9.0 - - GoLive.Generator.RazorPageRoute - SurgicalCoder - N/A - true - Generates strongly typed methods that return full URL for Razor and Blazor pages. - Copyright 2022 - SurgicalCoder - false - MIT - true - true - true - analyzers\cs - true - true - $(BaseIntermediateOutputPath)Generated - false - true + netstandard2.0 + 13 + true + analyzers\cs + true + true + $(BaseIntermediateOutputPath)Generated + true + true + en + GoLive.Generator.RazorPageRoute + SurgicalCoder + N/A + true + Generates strongly typed methods that return full URL for Razor and Blazor pages. + Copyright 2023 - 2025 - SurgicalCoder + false + MIT + true + true + true + true + true + $(BaseIntermediateOutputPath)Generated + false + true https://github.com/surgicalcoder/RazorPageRouteGenerator https://github.com/surgicalcoder/RazorPageRouteGenerator + git + 2.1.2-alpha + T:System.Range|T:System.Index|T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute|T:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths @@ -38,19 +55,19 @@ + + + + + - + @@ -69,4 +86,7 @@ + + + diff --git a/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.props b/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.props new file mode 100644 index 0000000..0ea0705 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/GoLive.Generator.RazorPageRoute.Generator.props @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/IsExternalInit.cs b/GoLive.Generator.RazorPageRoute.Generator/IsExternalInit.cs index 46aeec9..bd2b83b 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/IsExternalInit.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/IsExternalInit.cs @@ -1,4 +1,3 @@ -namespace System.Runtime.CompilerServices -{ - public class IsExternalInit { } -} \ No newline at end of file +namespace System.Runtime.CompilerServices; + +public class IsExternalInit { } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/PageRoute.cs b/GoLive.Generator.RazorPageRoute.Generator/PageRoute.cs index 0f495f8..d718861 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/PageRoute.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/PageRoute.cs @@ -1,9 +1,18 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis; -namespace GoLive.Generator.RazorPageRoute.Generator -{ - public record PageRoute(string Name, string Route, List QueryString); +namespace GoLive.Generator.RazorPageRoute.Generator; - public record PageRouteQuerystringParameter(string Name, ITypeSymbol Type); -} \ No newline at end of file +public record PageRoute(string Name, string Route, List QueryString, PageRouteAuth Auth = null); + +public class PageRouteAuth{ + public List Roles { get; set; } + public List Policies { get; set; } + public List AuthenticationSchemes { get; set; } + public bool RequiresAuthentication { get; set; } + public List CustomAuth { get; set; } +} + +public record PageRouteAuthCustomAuth(string Name, Dictionary CtorParams, Dictionary NamedParams); + +public record PageRouteQuerystringParameter(string Name, string Type); \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/PageRouteGenerator.cs b/GoLive.Generator.RazorPageRoute.Generator/PageRouteGenerator.cs deleted file mode 100644 index be7c85e..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/PageRouteGenerator.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - [Generator] - public class PageRouteGenerator : ISourceGenerator - { - private GeneratorExecutionContext executionContext; - private StringBuilder logBuilder; - private static SymbolDisplayFormat symbolDisplayFormat = new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); - public void Initialize(GeneratorInitializationContext context) - { - //if (!Debugger.IsAttached) - //{ - // Debugger.Launch(); - //} - } - - public void Execute(GeneratorExecutionContext context) - { - executionContext = context; - logBuilder = new StringBuilder(); - - - var config = LoadConfig(context, defaultNamespace:context.Compilation.Assembly.Name); - - if (config == null) - { - return; - } - - try - { - logBuilder.AppendLine("Output file : " + config.OutputToFile); - - string source = Generate(config, context); - logBuilder.AppendLine(source); - logBuilder.AppendLine(); - logBuilder.AppendLine(); - logBuilder.AppendLine(); - logBuilder.AppendLine(); - logBuilder.AppendLine(); - - if (!string.IsNullOrWhiteSpace(source)) - { - if (string.IsNullOrWhiteSpace(config.OutputToFile) && config.OutputToFiles.Count == 0) - { - context.AddSource("PageRoutes.g.cs", source); - } - else - { - if (config.OutputToFiles.Count > 0) - { - foreach (var configOutputToFile in config.OutputToFiles) - { - if (File.Exists(configOutputToFile)) - { - File.Delete(configOutputToFile); - } - - File.WriteAllText(configOutputToFile, source); - } - } - - if (!string.IsNullOrWhiteSpace(config.OutputToFile)) - { - if (File.Exists(config.OutputToFile)) - { - logBuilder.AppendLine("Output file exists, deleting"); - File.Delete(config.OutputToFile); - } - - File.WriteAllText(config.OutputToFile, source); - } - } - } - else - { - ReportEmptyFile(); - logBuilder.AppendLine("Source is empty"); - } - } - catch (Exception e) when (e.GetType() != typeof(IOException)) - { - ReportError(e, Location.None); - logBuilder.AppendLine(e.ToString()); - - - - throw; - } - catch (Exception e) - { - ReportError(e); - } - finally - { - if (!string.IsNullOrWhiteSpace(config.DebugOutputFile)) - { - File.WriteAllText(config.DebugOutputFile.Replace("(id)", Guid.NewGuid().ToString("N")), logBuilder.ToString()); - } - } - } - private readonly DiagnosticDescriptor _errorRuleWithLog = new DiagnosticDescriptor("RPG0001", "RPG0001: Error in source generator", "Error in source generator<{0}>: '{1}'. Log file details: '{2}'.", "SourceGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); - private readonly DiagnosticDescriptor _infoRule = new DiagnosticDescriptor("RPG0002", "RPG0002: Source code generated", "Source code generated<{0}>", "SourceGenerator", DiagnosticSeverity.Info, isEnabledByDefault: true); - private readonly DiagnosticDescriptor _emptyFileRule = new DiagnosticDescriptor("RPG0003", "RPG0003: Source code output is empty", "Source code was not outputted", "SourceGenerator", DiagnosticSeverity.Warning, isEnabledByDefault: true); - - private void ReportEmptyFile() - { - executionContext.ReportDiagnostic(Diagnostic.Create(_emptyFileRule, Location.None)); - } - - public void ReportInformation(Location? location = null) - { - if (location == null) - { - location = Location.None; - } - - executionContext.ReportDiagnostic(Diagnostic.Create(_infoRule, location, GetType().Name)); - } - - public void ReportError(Exception e, Location? location = null) - { - if (location == null) - { - location = Location.None; - } - executionContext.ReportDiagnostic(Diagnostic.Create(_errorRuleWithLog, location, GetType().Name, e.Message)); - } - - - private string Generate(Settings config, GeneratorExecutionContext context) - { - var source = new SourceStringBuilder(); - - if (config.OutputLastCreatedTime) - { - source.AppendLine($"// This file was generated on {DateTime.Now:R}"); - } - source.AppendLine("using System;"); - source.AppendLine("using System.Net.Http;"); - source.AppendLine("using System.Threading.Tasks;"); - source.AppendLine("using System.Net.Http.Json;"); - source.AppendLine("using System.Collections.Generic;"); - - if (config.OutputExtensionMethod) - { - source.AppendLine("using Microsoft.AspNetCore.Components;"); - } - - logBuilder.AppendLine("Adding namespaces"); - - var routes = context.Compilation.SyntaxTrees.Select(t => context.Compilation.GetSemanticModel(t)).Select(Scanner.ScanForPageRoutes).SelectMany(c => c).ToArray(); - logBuilder.AppendLine("Got Routes"); - source.AppendLine($"namespace {config.Namespace}"); - source.AppendOpenCurlyBracketLine(); - source.AppendLine($"public static partial class {config.ClassName}"); - source.AppendOpenCurlyBracketLine(); - - if (routes.Length == 0) - { - logBuilder.AppendLine("Routes is 0 length"); - return null; - } - - logBuilder.AppendLine("Foreach route"); - - foreach (var pageRoute in routes) - { - var routeTemplate = TemplateParser.ParseTemplate(pageRoute.Route); - - string SlugName = Slug.Create(pageRoute.Route.Length > 1 ? string.Join(".", routeTemplate.Segments.Where(f => !f.IsParameter).Select(f => f.Value)) : "Home"); - - var routeSegments = routeTemplate.Segments.Where(e => e.IsParameter).Select(delegate(TemplateSegment segment) - { - var constraint = segment.Constraints.Any() ? segment.Constraints.FirstOrDefault().GetConstraintType() : null; - - if (constraint == null) - { - return $"string {segment.Value}"; - } - - return segment.IsOptional ? $"{constraint.FullName}? {segment.Value}" : $"{constraint.FullName} {segment.Value}"; - }).ToList(); - - if (pageRoute.QueryString is {Count: > 0}) - { - routeSegments.AddRange(pageRoute.QueryString.Select(prqp => $"{prqp.Type.ToDisplayString()}{(prqp.Type.IsReferenceType ? "?" : "")} {prqp.Name} = default")); - } - - var parameterString = string.Join(", ", routeSegments); - - OutputRouteStringMethod(source, SlugName, parameterString, routeTemplate, pageRoute); - - if (config.OutputExtensionMethod) - { - OutputRouteExtensionMethod(source, SlugName, parameterString, routeTemplate, pageRoute); - } - } - logBuilder.AppendLine("Done, returning"); - source.AppendCloseCurlyBracketLine(); - source.AppendCloseCurlyBracketLine(); - - var outp = source.ToString(); - - return outp; - } - - - private void OutputRouteExtensionMethod(SourceStringBuilder source, string SlugName, string parameterString, RouteTemplate routeTemplate, PageRoute pageRoute) - { - logBuilder.AppendLine("OutputRouteExtensionMethod"); - - if (string.IsNullOrWhiteSpace(parameterString)) - { - source.AppendLine($"public static void {SlugName} (this NavigationManager manager, bool forceLoad = false, bool replace=false)"); - } - else - { - source.AppendLine($"public static void {SlugName} (this NavigationManager manager, {parameterString}, bool forceLoad = false, bool replace=false)"); - } - source.AppendOpenCurlyBracketLine(); - - if (routeTemplate.Segments.Any(e => e.IsParameter)) - { - source.AppendIndent(); - source.Append("string url = $\"", false); - - foreach (var seg in routeTemplate.Segments) - { - if (seg.IsParameter) - { - source.Append($"/{{{seg.Value}.ToString()}}", false); - } - else - { - source.Append($"/{seg.Value}", false); - } - } - - source.Append("\";\n", false); - } - else - { - source.AppendLine($"string url = \"{pageRoute.Route}\";"); - } - - if (pageRoute.QueryString is { Count: > 0 }) - { - source.AppendLine("Dictionary queryString=new();"); - - foreach (var pageRouteQuerystringParameter in pageRoute.QueryString) - { - if (pageRouteQuerystringParameter.Type.ToDisplayString(symbolDisplayFormat) == "System.String") - { - source.AppendLine($"if (!string.IsNullOrWhiteSpace({pageRouteQuerystringParameter.Name})) "); - } - else - { - source.AppendLine($"if ({pageRouteQuerystringParameter.Name} != default) "); - } - - source.AppendOpenCurlyBracketLine(); - source.AppendLine($"queryString.Add(\"{pageRouteQuerystringParameter.Name}\", {pageRouteQuerystringParameter.Name}.ToString());"); - source.AppendCloseCurlyBracketLine(); - } - - source.AppendLine(""); - source.AppendLine("url = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(url, queryString);"); - } - - source.AppendLine("manager.NavigateTo(url, forceLoad, replace);"); - source.AppendCloseCurlyBracketLine(); - } - - private void OutputRouteStringMethod(SourceStringBuilder source, string SlugName, string parameterString, RouteTemplate routeTemplate, PageRoute pageRoute) - { - logBuilder.AppendLine("Before creation #1"); - - source.AppendLine($"public static string {SlugName} ({parameterString})"); - source.AppendOpenCurlyBracketLine(); - logBuilder.AppendLine("Foreach Param"); - - if (routeTemplate.Segments.Any(e => e.IsParameter)) - { - source.AppendIndent(); - source.Append("string url = $\"", false); - - foreach (var seg in routeTemplate.Segments) - { - if (seg.IsParameter) - { - source.Append($"/{{{seg.Value}.ToString()}}", false); - } - else - { - source.Append($"/{seg.Value}", false); - } - } - - source.Append("\";\n", false); - } - else - { - source.AppendLine($"string url = \"{pageRoute.Route}\";"); - } - - if (pageRoute.QueryString is { Count: > 0 }) - { - source.AppendLine("Dictionary queryString=new();"); - - foreach (var pageRouteQuerystringParameter in pageRoute.QueryString) - { - if (pageRouteQuerystringParameter.Type.ToDisplayString(symbolDisplayFormat) == "System.String") - { - source.AppendLine($"if (!string.IsNullOrWhiteSpace({pageRouteQuerystringParameter.Name})) "); - } - else - { - source.AppendLine($"if ({pageRouteQuerystringParameter.Name} != default) "); - } - - source.AppendOpenCurlyBracketLine(); - source.AppendLine($"queryString.Add(\"{pageRouteQuerystringParameter.Name}\", {pageRouteQuerystringParameter.Name}.ToString());"); - source.AppendCloseCurlyBracketLine(); - } - - source.AppendLine(""); - source.AppendLine("url = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(url, queryString);"); - } - - source.AppendLine($"return url;"); - - source.AppendCloseCurlyBracketLine(); - } - - private Settings LoadConfig(GeneratorExecutionContext context, string defaultNamespace) - { - var configFilePath = context.AdditionalFiles.FirstOrDefault(e => e.Path.ToLowerInvariant().EndsWith("razorpageroutes.json")); - - if (configFilePath == null) - { - var settings = new Settings(); - settings.Namespace = defaultNamespace; - settings.ClassName = "PageRoutes"; - return settings; - } - - var jsonString = File.ReadAllText(configFilePath.Path); - var config = JsonSerializer.Deserialize(jsonString); - var configFileDirectory = Path.GetDirectoryName(configFilePath.Path); - - if (string.IsNullOrEmpty(config.Namespace)) - { - config.Namespace = defaultNamespace; - } - - if (!string.IsNullOrWhiteSpace(config.OutputToFile)) - { - var fullPath = Path.Combine(configFileDirectory, config.OutputToFile); - config.OutputToFile = Path.GetFullPath(fullPath); - } - - if (config.OutputToFiles != null && config.OutputToFiles.Any()) - { - config.OutputToFiles = config.OutputToFiles.Select(r => - { - var fullPath = Path.Combine(configFileDirectory, r); - return fullPath; - }).ToList(); - } - - return config; - } - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/PageRouteIncrementalExperimentalGenerator.cs b/GoLive.Generator.RazorPageRoute.Generator/PageRouteIncrementalExperimentalGenerator.cs new file mode 100644 index 0000000..38b9219 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/PageRouteIncrementalExperimentalGenerator.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using Data.Eval; +using GoLive.Generator.RazorPageRoute.Generator.Routing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mono.Cecil; + + +namespace GoLive.Generator.RazorPageRoute.Generator; + +[Generator] +public class PageRouteIncrementalExperimentalGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var defaultNamespace = context.AnalyzerConfigOptionsProvider.Select((provider, _) => !provider.GlobalOptions.TryGetValue("build_property.rootnamespace", out var ns) ? "DefaultNamespace" : ns); + + var projectDirProvider = context.AnalyzerConfigOptionsProvider.Select((provider, _) => + { + /*var globalOptions = new Dictionary(); + foreach (var option in provider.GlobalOptions.Keys) + { + if (provider.GlobalOptions.TryGetValue(option, out var value)) + { + globalOptions[option] = value; + } + }*/ + + if (!provider.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDir)) + { + return null; + } + return projectDir; + }); + + var configFiles = context.AdditionalTextsProvider.Where(IsConfigurationFile); + + var pageRouteItems = projectDirProvider.Combine(configFiles.Collect()).Select((projectPathAndConfigFiles, _) => + { + var (projectPath, configFiles) = projectPathAndConfigFiles; + + if (projectPath == null) + { + return default; + } + + var config = LoadConfig(configFiles, "DefaultNamespace"); + + var dllFile = Scanner.GetDllPathFromProject(projectPath, out var assemblyResolver); + + byte[] assemblyData = null; + int maxRetries = 5; + int currentTry = 0; + while (currentTry < maxRetries) + { + try + { + using (var fileStream = new FileStream(dllFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + { + assemblyData = new byte[fileStream.Length]; + fileStream.Read(assemblyData, 0, assemblyData.Length); + } + break; + } + catch (IOException) + { + currentTry++; + if (currentTry >= maxRetries) throw; + System.Threading.Thread.Sleep(150); + } + } + using var assembly = AssemblyDefinition.ReadAssembly(new MemoryStream(assemblyData), new ReaderParameters { AssemblyResolver = assemblyResolver }); + + var routes = Scanner.ScanForPageRoutesIncremental(assembly, config).CustomDistinctBy(e => e.Route).ToList(); + var incrementals = Scanner.ScanForInvokables(assembly).ToList(); + + return (routes, incrementals); + }); + + context.RegisterSourceOutput(pageRouteItems.Combine(configFiles.Collect()).Combine(defaultNamespace), Output); + } + + private void Output(SourceProductionContext productionContext, (((List routes, List<(string MethodName, string InvokableName)> incrementals) Left, ImmutableArray Right) Left, string defaultNamespace) arg2) + { + var config = LoadConfig(arg2.Left.Right, arg2.defaultNamespace); + + GenerateOutput(productionContext, config, arg2.Left.Left.routes); + + if (config.Invokables is { Enabled: true }) + { + GenerateJSInvokable(config, arg2.Left.Left.incrementals); + } + } + + public static void GenerateOutput(SourceProductionContext productionContext, Settings config, List pageRoutes) + { + var source = new SourceStringBuilder(); + + if (config.OutputLastCreatedTime) + { + source.AppendLine($"// This file was generated on {DateTime.Now:R}"); + } + + source.AppendLine("using System;"); + source.AppendLine("using System.Net.Http;"); + source.AppendLine("using System.Threading.Tasks;"); + source.AppendLine("using System.Net.Http.Json;"); + source.AppendLine("using System.Collections.Generic;"); + + if (config.OutputIAuthorizeData) + { + source.AppendLine("using Microsoft.AspNetCore.Authorization;"); + source.AppendLine("using Microsoft.AspNetCore.Components;"); + } + + if (config.OutputExtensionMethod) + { + source.AppendLine("using Microsoft.AspNetCore.Components;"); + } + + source.AppendLine($"namespace {config.Namespace}"); + source.AppendOpenCurlyBracketLine(); + source.AppendLine($"public static partial class {config.ClassName}"); + source.AppendOpenCurlyBracketLine(); + + if (pageRoutes.Count == 0) + { + return; + } + + foreach (var pageRoute in pageRoutes) + { + var routeTemplate = TemplateParser.ParseTemplate(pageRoute.Route); + + var SlugName = Slug.Create(pageRoute.Route.Length > 1 ? string.Join(".", routeTemplate.Segments.Where(f => !f.IsParameter).Select(f => f.Value)) : "Home"); + + if (string.IsNullOrWhiteSpace(SlugName)) + { + SlugName = pageRoute.Name; + } + + var routeSegments = routeTemplate.Segments.Where(e => e.IsParameter).Select(delegate(TemplateSegment segment) + { + var constraint = segment.Constraints.Any() ? segment.Constraints.FirstOrDefault().GetConstraintType() : null; + + if (constraint == null) + { + return $"string {segment.Value}"; + } + + return segment.IsOptional ? $"{constraint.FullName}? {segment.Value}" : $"{constraint.FullName} {segment.Value}"; + }).ToList(); + + if (pageRoute.QueryString is { Count: > 0 }) + { + routeSegments.AddRange(pageRoute.QueryString.Select(prqp => $"{prqp.Type} {prqp.Name} = default")); + } + + var parameterString = string.Join(", ", routeSegments); + + OutputRouteStringMethod(source, SlugName, parameterString, routeTemplate, pageRoute); + + if (config.OutputExtensionMethod) + { + OutputRouteExtensionMethod(source, SlugName, parameterString, routeTemplate, pageRoute, config); + } + } + + source.AppendCloseCurlyBracketLine(); + source.AppendCloseCurlyBracketLine(); + + var sourceOutput = source.ToString(); + + if (!string.IsNullOrWhiteSpace(sourceOutput)) + { + if (config.OutputToFiles.Count > 0) + { + foreach (var configOutputToFile in config.OutputToFiles) + { + File.WriteAllText(configOutputToFile, sourceOutput); + } + } + } + } + + private static void OutputRouteStringMethod(SourceStringBuilder source, string SlugName, string parameterString, RouteTemplate routeTemplate, PageRoute pageRoute) + { + source.AppendLine($"public static string {SlugName} ({parameterString})"); + source.AppendOpenCurlyBracketLine(); + + if (routeTemplate.Segments.Any(e => e.IsParameter)) + { + source.AppendIndent(); + source.Append("string url = $\"", false); + + foreach (var seg in routeTemplate.Segments) + { + if (seg.IsParameter) + { + source.Append($"/{{{seg.Value}.ToString()}}", false); + } + else + { + source.Append($"/{seg.Value}", false); + } + } + + source.Append("\";\n", false); + } + else + { + source.AppendLine($"string url = \"{pageRoute.Route}\";"); + } + + if (pageRoute.QueryString is { Count: > 0 }) + { + source.AppendLine("Dictionary queryString=new();"); + + foreach (var pageRouteQuerystringParameter in pageRoute.QueryString) + { + if (pageRouteQuerystringParameter.Type == "System.String" || pageRouteQuerystringParameter.Type.ToLower() == "string") + { + source.AppendLine($"if (!string.IsNullOrWhiteSpace({pageRouteQuerystringParameter.Name})) "); + } + else + { + source.AppendLine($"if ({pageRouteQuerystringParameter.Name} != default) "); + } + + source.AppendOpenCurlyBracketLine(); + source.AppendLine($"queryString.Add(\"{pageRouteQuerystringParameter.Name}\", {pageRouteQuerystringParameter.Name}.ToString());"); + source.AppendCloseCurlyBracketLine(); + } + + source.AppendLine(""); + source.AppendLine("url = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(url, queryString);"); + } + + source.AppendLine("return url;"); + + source.AppendCloseCurlyBracketLine(); + } + + + private static void OutputRouteExtensionMethod(SourceStringBuilder source, string SlugName, string parameterString, RouteTemplate routeTemplate, PageRoute pageRoute, Settings config) + { + if (pageRoute.Auth is { RequiresAuthentication: true }) + { + if (config.OutputIAuthorizeData) + { + source.AppendLine($"public class {SlugName}_AuthData : IAuthorizeData"); + source.AppendOpenCurlyBracketLine(); + + if (pageRoute.Auth.CustomAuth is { Count: > 0 }) + { + var authItem = pageRoute.Auth.CustomAuth.FirstOrDefault(); + var authSettings = config.Auth.First(e => e.Attribute == authItem.Name); + + var policyList = EvaluateCode(authSettings.PolicyTransformer, authItem); + var rolesList = EvaluateCode(authSettings.RolesTransformer, authItem); + var authSchemesList = EvaluateCode(authSettings.AuthenticationSchemeTransformer, authItem); + + source.AppendLine($"public string Policy {{ get; set; }} = {(string.Join(",", policyList ?? []).Length == 0 ? "String.Empty" : $"\"{string.Join(", ", policyList)}\"")}; "); + source.AppendLine($"public string Roles {{ get; set; }} = {(string.Join(",", rolesList ?? []).Length == 0 ? "String.Empty" : $"\"{string.Join(", ", rolesList)}\"")}; "); + source.AppendLine($"public string AuthenticationSchemes {{ get; set; }} = {(string.Join(",", authSchemesList ?? []).Length == 0 ? "String.Empty" : $"\"{string.Join(", ", authSchemesList)}\"")}; "); + } + else + { + source.AppendLine($"public string Policy {{ get; set; }} = {(string.Join(",", pageRoute.Auth.Policies ?? []).Length == 0 ? "String.Empty" : $"\"{string.Join(",", pageRoute.Auth.Policies)}\"")};"); + source.AppendLine($"public string Roles {{ get; set; }} = {(string.Join(",", pageRoute.Auth.Roles ?? []).Length == 0 ? "String.Empty" : $"\"{string.Join(",", pageRoute.Auth.Roles)}\"")}; "); + source.AppendLine($"public string AuthenticationSchemes {{ get; set; }} = {(string.Join(",", pageRoute.Auth.AuthenticationSchemes ?? []).Length == 0 ? "String.Empty" : $"\"{string.Join(", ", pageRoute.Auth.AuthenticationSchemes)}\"")}; "); + } + + source.AppendCloseCurlyBracketLine(); + } + + + source.AppendLine("/// "); + source.AppendLine($"/// Page Requires Authentication{(pageRoute.Auth.CustomAuth != null ? ", Custom Authentication Provider (CustomAuth)" : "")}"); + + if (pageRoute.Auth.Roles != null && pageRoute.Auth.Roles.Any()) + { + source.AppendLine($"/// Roles: {string.Join(", ", pageRoute.Auth.Roles)}"); + } + + if (pageRoute.Auth.Policies != null && pageRoute.Auth.Policies.Any()) + { + source.AppendLine($"/// Policies: {string.Join(", ", pageRoute.Auth.Policies)}"); + } + + if (pageRoute.Auth.CustomAuth != null) + { + foreach (var customAuth in pageRoute.Auth.CustomAuth) + { + source.AppendLine($"/// Custom Authentication ProviderName: {customAuth.Name}"); + source.AppendLine($"/// Custom Auth Constructor Params: {string.Join(", ", customAuth.CtorParams)}"); + source.AppendLine($"/// Custom Auth Named Params: {string.Join(", ", customAuth.NamedParams)}"); + } + } + + source.AppendLine("/// "); + } + + if (string.IsNullOrWhiteSpace(parameterString)) + { + source.AppendLine($"public static void {SlugName} (this NavigationManager manager, bool forceLoad = false, bool replace=false)"); + } + else + { + source.AppendLine($"public static void {SlugName} (this NavigationManager manager, {parameterString}, bool forceLoad = false, bool replace=false)"); + } + + source.AppendOpenCurlyBracketLine(); + + if (routeTemplate.Segments.Any(e => e.IsParameter)) + { + source.AppendIndent(); + source.Append("string url = $\"", false); + + foreach (var seg in routeTemplate.Segments) + { + if (seg.IsParameter) + { + source.Append($"/{{{seg.Value}.ToString()}}", false); + } + else + { + source.Append($"/{seg.Value}", false); + } + } + + source.Append("\";\n", false); + } + else + { + source.AppendLine($"string url = \"{pageRoute.Route}\";"); + } + + if (pageRoute.QueryString is { Count: > 0 }) + { + source.AppendLine("Dictionary queryString=new();"); + + foreach (var pageRouteQuerystringParameter in pageRoute.QueryString) + { + if (pageRouteQuerystringParameter.Type == "System.String" || pageRouteQuerystringParameter.Type.ToLower() == "string") + { + source.AppendLine($"if (!string.IsNullOrWhiteSpace({pageRouteQuerystringParameter.Name})) "); + } + else + { + source.AppendLine($"if ({pageRouteQuerystringParameter.Name} != default) "); + } + + source.AppendOpenCurlyBracketLine(); + source.AppendLine($"queryString.Add(\"{pageRouteQuerystringParameter.Name}\", {pageRouteQuerystringParameter.Name}.ToString());"); + source.AppendCloseCurlyBracketLine(); + } + + source.AppendLine(""); + source.AppendLine("url = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(url, queryString);"); + } + + source.AppendLine("manager.NavigateTo(url, forceLoad, replace);"); + source.AppendCloseCurlyBracketLine(); + } + + private static List EvaluateCode(string transformer, PageRouteAuthCustomAuth authItem) + { + if (string.IsNullOrWhiteSpace(transformer)) + { + return null; + } + Evaluator policyEvaluator = new(transformer); + policyEvaluator.AddUsing("System.Collections.Generic"); + policyEvaluator["ConstructorParameters"] = authItem.CtorParams; + policyEvaluator["NamedParameters"] = authItem.NamedParams; + var policyList = policyEvaluator.Eval>(); + return policyList; + } + + + public static Settings LoadConfig(IEnumerable configFiles, string defaultNamespace) + { + var configFilePath = configFiles.FirstOrDefault(); + + if (configFilePath == null) + { + return null; + } + + var filePath = configFilePath.Path; + return LoadConfigFromFile(filePath, defaultNamespace); + } + + public static Settings LoadConfigFromFile(string filePath, string defaultNamespace) + { + var jsonString = File.ReadAllText(filePath); + var config = JsonSerializer.Deserialize(jsonString); + var configFileDirectory = Path.GetDirectoryName(filePath); + + if (string.IsNullOrEmpty(config.Namespace)) + { + config.Namespace = defaultNamespace; + } + + if (config.OutputToFiles != null && config.OutputToFiles.Any()) + { + config.OutputToFiles = config.OutputToFiles.Select(r => + { + var fullPath = Path.Combine(configFileDirectory, r); + + return fullPath; + }).ToList(); + } + + if (config.Invokables != null && config.Invokables.OutputToFiles.Count > 0) + { + foreach (var outputFile in config.Invokables.OutputToFiles) + { + var fullPath = Path.Combine(configFileDirectory, outputFile); + var index = config.Invokables.OutputToFiles.IndexOf(outputFile); + config.Invokables.OutputToFiles[index] = Path.GetFullPath(fullPath); + } + } + + return config; + } + + public static void GenerateJSInvokable(Settings config, List<(string MethodName, string InvokableName)> invokables) + { + if (invokables.Count == 0) + { + return; + } + + var jsBuilder = new StringBuilder(); + jsBuilder.AppendLine($"const {config.Invokables.JSClassName} = {{"); + + foreach (var (methodName, invokableName) in invokables) + { + jsBuilder.AppendLine($"{methodName.Replace(".","_")}: \"{invokableName}\", "); + } + + jsBuilder.AppendLine("};"); + + if (config.Invokables.OutputToFiles.Count > 0) + { + foreach (var outputPath in config.Invokables.OutputToFiles) + { + File.WriteAllText(outputPath, jsBuilder.ToString()); + } + } + } + + private static bool IsConfigurationFile(AdditionalText text) => text.Path.EndsWith("RazorPageRoutes.json"); +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Properties/launchSettings.json b/GoLive.Generator.RazorPageRoute.Generator/Properties/launchSettings.json index cc0b9dc..bc7b038 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/Properties/launchSettings.json +++ b/GoLive.Generator.RazorPageRoute.Generator/Properties/launchSettings.json @@ -1,9 +1,17 @@ -{ +{ "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "GoLive.Generator.RazorPageRoute.Generator": { "commandName": "DebugRoslynComponent", - "targetProject": "../GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly.csproj" + "targetProject": "../GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly/GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly.csproj" + }, + "Web3": { + "commandName": "DebugRoslynComponent", + "targetProject": "../GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly3/GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly3.csproj" + }, + "Web4": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../solutionfactory.core.mongotemplate/SolutionFactoryMVC.Web/SolutionFactoryMVC.Web.csproj" } } } diff --git a/GoLive.Generator.RazorPageRoute.Generator/RouteAttributeExtractor.cs b/GoLive.Generator.RazorPageRoute.Generator/RouteAttributeExtractor.cs new file mode 100644 index 0000000..fb7d374 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/RouteAttributeExtractor.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace GoLive.Generator.RazorPageRoute.Generator; + +public class RouteAttributeExtractor +{ + public class ExtractedRoutes + { + public string ClassName { get; set; } + public List Routes { get; set; } + public List QueryString { get; set; } + } + + public static List ExtractRoutes(string projectPath) + { + var extractedRoutes = new List(); + var files = Directory.GetFiles(projectPath, "*.cs", SearchOption.AllDirectories); + + foreach (var file in files) + { + var code = File.ReadAllText(file); + var tree = CSharpSyntaxTree.ParseText(code); + var root = tree.GetRoot(); + + var classDeclarations = root.DescendantNodes().OfType(); + + foreach (var classDeclaration in classDeclarations) + { + var className = classDeclaration.Identifier.Text; + var routes = new List(); + var queryString = new List(); + + var attributes = classDeclaration.DescendantNodes() + .OfType() + .Where(attr => attr.Name.ToString().Contains("RouteAttribute")); + + foreach (var attribute in attributes) + { + var argument = attribute.ArgumentList?.Arguments.FirstOrDefault(); + + if (argument != null) + { + var route = argument.ToString().Trim('"'); + routes.Add(route); + } + } + + var properties = classDeclaration.DescendantNodes().OfType(); + foreach (var property in properties) + { + var hasQueryAttribute = property.AttributeLists + .SelectMany(attrList => attrList.Attributes) + .Any(attr => attr.Name.ToString().Contains("SupplyParameterFromQueryAttribute") || attr.Name.ToString().Contains("SupplyParameterFromQuery")); + + if (hasQueryAttribute) + { + var propertyName = property.Identifier.Text; + var propertyType = property.Type.ToString(); + queryString.Add(new PageRouteQuerystringParameter(propertyName, propertyType)); + } + } + + if (routes.Any()) + { + extractedRoutes.Add(new ExtractedRoutes { ClassName = className, Routes = routes, QueryString = queryString }); + } + } + } + + return extractedRoutes; + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/RouteConstraint.cs b/GoLive.Generator.RazorPageRoute.Generator/RouteConstraint.cs deleted file mode 100644 index 1421886..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/RouteConstraint.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - internal static class RouteConstraint - { - public static UrlValueConstraint Parse(string template, string segment, string constraint) - { - if (string.IsNullOrEmpty(constraint)) - { - throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint."); - } - - var targetType = GetTargetType(constraint); - if (targetType is null || !UrlValueConstraint.TryGetByTargetType(targetType, out var result)) - { - throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'."); - } - - return result; - } - - private static Type? GetTargetType(string constraint) => constraint switch - { - "bool" => typeof(bool), - "datetime" => typeof(DateTime), - "decimal" => typeof(decimal), - "double" => typeof(double), - "float" => typeof(float), - "guid" => typeof(Guid), - "int" => typeof(int), - "long" => typeof(long), - _ => null, - }; - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/RouteTemplate.cs b/GoLive.Generator.RazorPageRoute.Generator/RouteTemplate.cs deleted file mode 100644 index 42daa77..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/RouteTemplate.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Diagnostics; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - [DebuggerDisplay("{TemplateText}")] - internal class RouteTemplate - { - public RouteTemplate(string templateText, TemplateSegment[] segments) - { - TemplateText = templateText; - Segments = segments; - - for (var i = 0; i < segments.Length; i++) - { - var segment = segments[i]; - if (segment.IsOptional) - { - OptionalSegmentsCount++; - } - if (segment.IsCatchAll) - { - ContainsCatchAllSegment = true; - } - } - } - - public string TemplateText { get; } - - public TemplateSegment[] Segments { get; } - - public int OptionalSegmentsCount { get; } - - public bool ContainsCatchAllSegment { get; } - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Routing/RouteConstraint.cs b/GoLive.Generator.RazorPageRoute.Generator/Routing/RouteConstraint.cs new file mode 100644 index 0000000..edfa7ad --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Routing/RouteConstraint.cs @@ -0,0 +1,36 @@ +using System; + +namespace GoLive.Generator.RazorPageRoute.Generator.Routing; + +internal static class RouteConstraint +{ + public static UrlValueConstraint Parse(string template, string segment, string constraint) + { + if (string.IsNullOrEmpty(constraint)) + { + throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint."); + } + + var targetType = GetTargetType(constraint); + if (targetType is null || !UrlValueConstraint.TryGetByTargetType(targetType, out var result)) + { + throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'."); + } + + return result; + } + + private static Type? GetTargetType(string constraint) => constraint switch + { + "bool" => typeof(bool), + "datetime" => typeof(DateTime), + "decimal" => typeof(decimal), + "double" => typeof(double), + "float" => typeof(float), + "guid" => typeof(Guid), + "int" => typeof(int), + "long" => typeof(long), + "nonfile" => typeof(string), + _ => null, + }; +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Routing/RouteTemplate.cs b/GoLive.Generator.RazorPageRoute.Generator/Routing/RouteTemplate.cs new file mode 100644 index 0000000..46d14a8 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Routing/RouteTemplate.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; + +namespace GoLive.Generator.RazorPageRoute.Generator.Routing; + +[DebuggerDisplay("{TemplateText}")] +internal class RouteTemplate +{ + public RouteTemplate(string templateText, TemplateSegment[] segments) + { + TemplateText = templateText; + Segments = segments; + + for (var i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + if (segment.IsOptional) + { + OptionalSegmentsCount++; + } + if (segment.IsCatchAll) + { + ContainsCatchAllSegment = true; + } + } + } + + public string TemplateText { get; } + + public TemplateSegment[] Segments { get; } + + public int OptionalSegmentsCount { get; } + + public bool ContainsCatchAllSegment { get; } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Routing/StringSegmentAccumulator.cs b/GoLive.Generator.RazorPageRoute.Generator/Routing/StringSegmentAccumulator.cs new file mode 100644 index 0000000..b1ee660 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Routing/StringSegmentAccumulator.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace GoLive.Generator.RazorPageRoute.Generator.Routing; + +internal struct StringSegmentAccumulator +{ + private int count; + private ReadOnlyMemory _single; + private List>? _multiple; + + public ReadOnlyMemory this[int index] + { + get + { + if (index >= count) + { + throw new IndexOutOfRangeException(); + } + + return count == 1 ? _single : _multiple![index]; + } + } + + public int Count => count; + + public void SetSingle(ReadOnlyMemory value) + { + _single = value; + + if (count != 1) + { + if (count > 1) + { + _multiple = null; + } + + count = 1; + } + } + + public void Add(ReadOnlyMemory value) + { + switch (count++) + { + case 0: + _single = value; + break; + case 1: + _multiple = new(); + _multiple.Add(_single); + _multiple.Add(value); + _single = default; + break; + default: + _multiple!.Add(value); + break; + } + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Routing/TemplateParser.cs b/GoLive.Generator.RazorPageRoute.Generator/Routing/TemplateParser.cs new file mode 100644 index 0000000..92e4714 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Routing/TemplateParser.cs @@ -0,0 +1,103 @@ +using System; + +namespace GoLive.Generator.RazorPageRoute.Generator.Routing; + +internal class TemplateParser +{ + public static readonly char[] InvalidParameterNameCharacters = + new char[] { '{', '}', '=', '.' }; + + internal static RouteTemplate ParseTemplate(string template) + { + var originalTemplate = template; + template = template.Trim('/'); + if (template == string.Empty) + { + // Special case "/"; + return new RouteTemplate("/", Array.Empty()); + } + + var segments = template.Split('/'); + var templateSegments = new TemplateSegment[segments.Length]; + for (int i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + if (string.IsNullOrEmpty(segment)) + { + throw new InvalidOperationException( + $"Invalid template '{template}'. Empty segments are not allowed."); + } + + if (segment[0] != '{') + { + if (segment[segment.Length - 1] == '}') + { + throw new InvalidOperationException( + $"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'."); + } + if (segment.Substring(segment.Length - 1) == "?") + { + throw new InvalidOperationException( + $"Invalid template '{template}'. '?' is not allowed in literal segment '{segment}'."); + } + templateSegments[i] = new TemplateSegment(originalTemplate, segment, isParameter: false); + } + else + { + if (segment[segment.Length - 1] != '}') + { + throw new InvalidOperationException( + $"Invalid template '{template}'. Missing '}}' in parameter segment '{segment}'."); + } + + if (segment.Length < 3) + { + throw new InvalidOperationException( + $"Invalid template '{template}'. Empty parameter name in segment '{segment}' is not allowed."); + } + + var invalidCharacter = segment.IndexOfAny(InvalidParameterNameCharacters, 1, segment.Length - 2); + if (invalidCharacter != -1) + { + throw new InvalidOperationException( + $"Invalid template '{template}'. The character '{segment[invalidCharacter]}' in parameter segment '{segment}' is not allowed."); + } + + templateSegments[i] = new TemplateSegment(originalTemplate, segment.Substring(1, segment.Length - 2), isParameter: true); + } + } + + for (int i = 0; i < templateSegments.Length; i++) + { + var currentSegment = templateSegments[i]; + + if (currentSegment.IsCatchAll && i != templateSegments.Length - 1) + { + throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter can only appear as the last segment of the route template."); + } + + if (!currentSegment.IsParameter) + { + continue; + } + + for (int j = i + 1; j < templateSegments.Length; j++) + { + var nextSegment = templateSegments[j]; + + if (currentSegment.IsOptional && !nextSegment.IsOptional && !nextSegment.IsCatchAll) + { + throw new InvalidOperationException($"Invalid template '{template}'. Non-optional parameters or literal routes cannot appear after optional parameters."); + } + + if (string.Equals(currentSegment.Value, nextSegment.Value, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"Invalid template '{template}'. The parameter '{currentSegment}' appears multiple times."); + } + } + } + + return new RouteTemplate(template, templateSegments); + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Routing/TemplateSegment.cs b/GoLive.Generator.RazorPageRoute.Generator/Routing/TemplateSegment.cs new file mode 100644 index 0000000..1353313 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Routing/TemplateSegment.cs @@ -0,0 +1,142 @@ +using System; + +namespace GoLive.Generator.RazorPageRoute.Generator.Routing; + +internal class TemplateSegment +{ + public TemplateSegment(string template, string segment, bool isParameter) + { + IsParameter = isParameter; + + IsCatchAll = isParameter && segment.StartsWith('*'.ToString()); + + if (IsCatchAll) + { + // Only one '*' currently allowed + Value = segment.Substring(1); + + var invalidCharacterIndex = Value.IndexOf('*'); + if (invalidCharacterIndex != -1) + { + throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment."); + } + } + else + { + Value = segment; + } + + // Process segments that parameters that do not contain a token separating a type constraint. + if (IsParameter) + { + if (Value.IndexOf(':') < 0) + { + + // Set the IsOptional flag to true for segments that contain + // a parameter with no type constraints but optionality set + // via the '?' token. + var questionMarkIndex = Value.IndexOf('?'); + if (questionMarkIndex == Value.Length - 1) + { + IsOptional = true; + Value = Value.Substring(0, Value.Length - 1); + } + // If the `?` optional marker shows up in the segment but not at the very end, + // then throw an error. + else if (questionMarkIndex >= 0) + { + throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name."); + } + + Constraints = Array.Empty(); + } + else + { + var tokens = Value.Split(':'); + if (tokens[0].Length == 0) + { + throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list."); + } + + Value = tokens[0]; + IsOptional = tokens[tokens.Length - 1].EndsWith('?'.ToString()); + if (IsOptional) + { + tokens[tokens.Length - 1] = tokens[tokens.Length - 1].Substring(0, tokens[tokens.Length - 1].Length - 1); + } + + Constraints = new UrlValueConstraint[tokens.Length - 1]; + for (var i = 1; i < tokens.Length; i++) + { + Constraints[i - 1] = RouteConstraint.Parse(template, segment, tokens[i]); + } + } + } + else + { + Constraints = Array.Empty(); + } + + if (IsParameter) + { + if (IsOptional && IsCatchAll) + { + throw new InvalidOperationException($"Invalid segment '{segment}' in route '{template}'. A catch-all parameter cannot be marked optional."); + } + + // Moving the check for this here instead of TemplateParser so we can allow catch-all. + // We checked for '*' up above specifically for catch-all segments, this one checks for all others + if (Value.IndexOf('*') != -1) + { + throw new InvalidOperationException($"Invalid template '{template}'. The character '*' in parameter segment '{{{segment}}}' is not allowed."); + } + } + } + + // The value of the segment. The exact text to match when is a literal. + // The parameter name when its a segment + public string Value { get; } + + public bool IsParameter { get; } + + public bool IsOptional { get; } + + public bool IsCatchAll { get; } + + public UrlValueConstraint[] Constraints { get; } + + public bool Match(string pathSegment, out object? matchedParameterValue) + { + if (IsParameter) + { + matchedParameterValue = pathSegment; + + foreach (var constraint in Constraints) + { + if (!constraint.TryParse(pathSegment, out matchedParameterValue)) + { + return false; + } + } + + return true; + } + else + { + matchedParameterValue = null; + return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase); + } + } + + public override string ToString() => this switch + { + { IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: 0 } } => $"{{{Value}}}", + { IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':'.ToString(), (object[])Constraints)}}}", + { IsParameter: true, IsOptional: true, Constraints: { Length: 0 } } => $"{{{Value}?}}", + { IsParameter: true, IsOptional: true, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':'.ToString(), (object[])Constraints)}?}}", + { IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => $"{{*{Value}}}", + { IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => $"{{*{Value}:{string.Join(':'.ToString(), (object[])Constraints)}?}}", + { IsParameter: false } => Value, + _ => throw new InvalidOperationException("Invalid template segment.") + }; +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Routing/UrlValueConstraint.cs b/GoLive.Generator.RazorPageRoute.Generator/Routing/UrlValueConstraint.cs new file mode 100644 index 0000000..fcfd5c5 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/Routing/UrlValueConstraint.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; + +namespace GoLive.Generator.RazorPageRoute.Generator.Routing; + +/// +/// Shared logic for parsing tokens from route values and querystring values. +/// +internal abstract class UrlValueConstraint +{ + public delegate bool TryParseDelegate(string str, out T result); + + private static readonly ConcurrentDictionary _cachedInstances = new(); + + public static bool TryGetByTargetType(Type targetType, out UrlValueConstraint result) + { + if (!_cachedInstances.TryGetValue(targetType, out result)) + { + result = Create(targetType); + if (result is null) + { + return false; + } + + _cachedInstances.TryAdd(targetType, result); + } + + return true; + } + + private static bool TryParse(string str, out string result) + { + result = str.ToString(); + return true; + } + + private static bool TryParse(string str, out DateTime result) + => DateTime.TryParse(str.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out result); + + private static bool TryParse(string str, out decimal result) + => decimal.TryParse(str.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(string str, out double result) + => double.TryParse(str.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(string str, out float result) + => float.TryParse(str.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(string str, out int result) + => int.TryParse(str.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + + private static bool TryParse(string str, out long result) + => long.TryParse(str.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result); + + private static UrlValueConstraint? Create(Type targetType) => targetType switch + { + var x when x == typeof(string) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(bool) => new TypedUrlValueConstraint(bool.TryParse), + var x when x == typeof(bool?) => new NullableTypedUrlValueConstraint(bool.TryParse), + var x when x == typeof(DateTime) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(DateTime?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(decimal) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(decimal?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(double) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(double?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(float) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(float?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(Guid) => new TypedUrlValueConstraint(Guid.TryParse), + var x when x == typeof(Guid?) => new NullableTypedUrlValueConstraint(Guid.TryParse), + var x when x == typeof(int) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(int?) => new NullableTypedUrlValueConstraint(TryParse), + var x when x == typeof(long) => new TypedUrlValueConstraint(TryParse), + var x when x == typeof(long?) => new NullableTypedUrlValueConstraint(TryParse), + var x => null + }; + + public abstract Type GetConstraintType(); + + public abstract bool TryParse(string value, out object result); + + public abstract object? Parse(string value, string destinationNameForMessage); + + public abstract Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage); + + private class TypedUrlValueConstraint : UrlValueConstraint + { + private readonly TryParseDelegate _parser; + + public TypedUrlValueConstraint(TryParseDelegate parser) + { + _parser = parser; + } + + public override Type GetConstraintType() + { + return typeof(T); + } + + public override bool TryParse(string value, out object result) + { + if (_parser(value, out var typedResult)) + { + result = typedResult!; + return true; + } + else + { + result = null; + return false; + } + } + + public override object? Parse(string value, string destinationNameForMessage) + { + if (!_parser(value, out var parsedValue)) + { + throw new InvalidOperationException($"Cannot parse the value '{value.ToString()}' as type '{typeof(T)}' for '{destinationNameForMessage}'."); + } + + return parsedValue; + } + + public override Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage) + { + var count = values.Count; + if (count == 0) + { + return Array.Empty(); + } + + var result = new T?[count]; + + for (var i = 0; i < count; i++) + { + if (!_parser(values[i].ToString(), out result[i])) + { + throw new InvalidOperationException($"Cannot parse the value '{values[i]}' as type '{typeof(T)}' for '{destinationNameForMessage}'."); + } + } + + return result; + } + } + + private sealed class NullableTypedUrlValueConstraint : TypedUrlValueConstraint where T : struct + { + public NullableTypedUrlValueConstraint(TryParseDelegate parser) + : base(SupportNullable(parser)) + { + } + + private static TryParseDelegate SupportNullable(TryParseDelegate parser) + { + return TryParseNullable; + + bool TryParseNullable(string value, out T? result) + { + if (string.IsNullOrWhiteSpace(value)) + { + result = default; + return true; + } + else if (parser(value, out var parsedValue)) + { + result = parsedValue; + return true; + } + else + { + result = default; + return false; + } + } + } + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Scanner.cs b/GoLive.Generator.RazorPageRoute.Generator/Scanner.cs index 3027cbc..a6b1ba3 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/Scanner.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/Scanner.cs @@ -1,74 +1,243 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Mono.Cecil; -namespace GoLive.Generator.RazorPageRoute.Generator -{ - public static class Scanner - { - public static IEnumerable ScanForPageRoutes(SemanticModel semantic) - { - var baseClass = semantic.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.ComponentBase"); +namespace GoLive.Generator.RazorPageRoute.Generator; - if (baseClass == null) - { - yield break; - } +public static class Scanner +{ + private static readonly string componentBaseTypeName = "Microsoft.AspNetCore.Components.ComponentBase"; + public const string jsInvokableAttribute = "Microsoft.JSInterop.JSInvokableAttribute"; - var allNodes = semantic.SyntaxTree.GetRoot().DescendantNodes().OfType(); + public static IEnumerable<(string MethodName, string InvokableName)> ScanForInvokables(AssemblyDefinition input) + { + var types = input.MainModule.Types + .Where(t => t.BaseType != null && t is { IsClass: true, IsAbstract: false } && + IsSubclassOf(t, componentBaseTypeName)) + .ToList(); - foreach (var node in allNodes) + foreach (var type in types) + { + if (IsSubclassOf(type, componentBaseTypeName)) { - if (semantic.GetDeclaredSymbol(node) is INamedTypeSymbol classSymbol && InheritsFrom(classSymbol, baseClass)) + foreach (var method in type.Methods) { - var res = ToRoute(classSymbol); + // Check if the method has the attribute [JSInvokable(string)] + var jsInvokableAttr = method.CustomAttributes.FirstOrDefault(attr => + attr.AttributeType.FullName == jsInvokableAttribute && + attr.ConstructorArguments.Count > 0 && + attr.ConstructorArguments[0].Value is string); - foreach (var pageRoute in res) + if (jsInvokableAttr != null) { - yield return pageRoute; + // Extract method name + string methodName = method.Name; + + // Extract identifier in the attribute + string identifier = jsInvokableAttr.ConstructorArguments[0].Value.ToString(); + + yield return ($"{type.FullName}.{method.Name}", identifier); } } } } - - private static IEnumerable ToRoute(INamedTypeSymbol classSymbol) + } + + public static string GetDllPathFromProject(string projectPath, out DefaultAssemblyResolver assemblyResolver, string[] additionalSearchDirectories = null) + { + var debugPath = getHighestFolderVersion(Path.Combine(projectPath, "bin", "Debug")); + + var objDebugPath = getHighestFolderVersion(Path.Combine(projectPath, "obj", "Debug")); + var refIntPath = Path.Combine(objDebugPath, "refInt"); + + string dllFile = null; + int retries = 3; + while (retries > 0) { - var attributes = FindAttributes(classSymbol, a => a.ToString() == "Microsoft.AspNetCore.Components.RouteAttribute"); + dllFile = Directory.GetFiles(refIntPath, "*.dll").FirstOrDefault(); + if (dllFile != null) + { + break; + } + retries--; + System.Threading.Thread.Sleep(150); + } - var queryStringParams = classSymbol.GetMembers().OfType(); - var querystringParameters = (from qsParam in queryStringParams let qsAttr = FindAttribute(qsParam, a => a.ToString() == "Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute") where qsAttr != null select new PageRouteQuerystringParameter(qsAttr?.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? qsParam.Name, qsParam.Type)).ToList(); + if (dllFile == null) + { + throw new FileNotFoundException("No DLL file found in refInt directory."); + } - foreach (var attributeData in attributes) + assemblyResolver = new DefaultAssemblyResolver(); + assemblyResolver.AddSearchDirectory(refIntPath); + if (additionalSearchDirectories != null) + { + foreach (var searchDirectory in additionalSearchDirectories) { - var route = attributeData?.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? string.Empty; - - yield return new PageRoute(classSymbol.Name, route, querystringParameters); + assemblyResolver.AddSearchDirectory(searchDirectory); } } + //assemblyResolver.AddSearchDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet", "shared", "Microsoft.AspNetCore.App", "9.0.0")); + assemblyResolver.AddSearchDirectory(debugPath); + + return dllFile; + } + + public static IEnumerable ScanForPageRoutesIncremental(AssemblyDefinition input, Settings settings) + { + var types = input.MainModule.Types + .Where(t => t.BaseType != null && t is { IsClass: true, IsAbstract: false } && + IsSubclassOf(t, componentBaseTypeName)) + .ToList(); + + foreach (var pageRoute in types.Select(definition => ToRoute(definition, settings)).SelectMany(res => res)) + { + yield return pageRoute; + } + } + + private static IEnumerable ToRoute(TypeDefinition input, Settings settings) + { + var classAttributes = GetAttributes(input.CustomAttributes); + + var routes = classAttributes + .Where(attr => attr.AttributeType.FullName == "Microsoft.AspNetCore.Components.RouteAttribute") + .Select(attr => attr.ConstructorArguments.FirstOrDefault().Value?.ToString()) + .Where(route => !string.IsNullOrEmpty(route)) + .ToList(); + + var queryStringParams = input.Properties; + + var querystringParameters = queryStringParams + .Where(p => p.CustomAttributes.Any(attr => + attr.AttributeType.FullName == "Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute")) + .Select(p => new PageRouteQuerystringParameter(p.Name, p.PropertyType.FullName)) + .ToList(); + + var pageRouteAuth = getPageRouteAuth(input, settings); + + foreach (var route in routes) + { + yield return new PageRoute(input.Name, route, querystringParameters, pageRouteAuth); + } + } + + private static PageRouteAuth getPageRouteAuth(TypeDefinition input, Settings settings) + { + PageRouteAuth retr = new(); + + // Check for built-in Authorize attributes + var authorizeAttributes = input.CustomAttributes + .Where(attr => attr.AttributeType.FullName == "Microsoft.AspNetCore.Authorization.AuthorizeAttribute") + .ToList(); + + foreach (var attr in authorizeAttributes) + { + retr.RequiresAuthentication = true; - private static IEnumerable FindAttributes(ISymbol symbol, Func selectAttribute) => symbol.GetAttributes().Where(a => a?.AttributeClass != null && selectAttribute(a.AttributeClass)); + foreach (var arg in attr.ConstructorArguments) + { + if (arg.Type.FullName == "System.String") + { + retr.Roles = arg.Value.ToString().Split(',').Select(role => role.Trim()).ToList(); + } + } - private static AttributeData? FindAttribute(ISymbol symbol, Func selectAttribute) => symbol.GetAttributes().LastOrDefault(a => a?.AttributeClass != null && selectAttribute(a.AttributeClass)); + foreach (var namedArg in attr.Properties) + { + if (namedArg.Name == "Roles") + { + retr.Roles = namedArg.Argument.Value.ToString().Split(',').Select(role => role.Trim()).ToList(); + } + else if (namedArg.Name == "Policy") + { + retr.Policies = namedArg.Argument.Value.ToString().Split(',').Select(policy => policy.Trim()).ToList(); + } + else if (namedArg.Name == "AuthenticationSchemes") + { + retr.AuthenticationSchemes = namedArg.Argument.Value.ToString().Split(',').Select(scheme => scheme.Trim()).ToList(); + } + } + } - private static bool InheritsFrom(INamedTypeSymbol classDeclaration, INamedTypeSymbol targetBaseType) + // Check for custom attributes from settings.Auth + foreach (var customAuth in settings.Auth) { - var currentDeclared = classDeclaration; + var customAttributes = input.CustomAttributes + .Where(attr => attr.AttributeType.FullName == customAuth.Attribute) + .ToList(); - while (currentDeclared.BaseType != null) + foreach (var attr in customAttributes) { - var currentBaseType = currentDeclared.BaseType; + retr.RequiresAuthentication = true; + + var ctorArgs = new Dictionary(); + var namedArgs = new Dictionary(); - if (currentBaseType.Equals(targetBaseType, SymbolEqualityComparer.Default)) + foreach (var arg in attr.ConstructorArguments) { - return true; + var constructor = attr.AttributeType.Resolve().Methods + .First(m => m.IsConstructor && m.Parameters.Count == attr.ConstructorArguments.Count); + + var parameterName = constructor.Parameters[attr.ConstructorArguments.IndexOf(arg)].Name; + var parameterValue = arg.Value is CustomAttributeArgument[] array + ? string.Join(",", array.Select(a => a.Value?.ToString())) + : arg.Value?.ToString(); + + if (parameterValue != null) + { + ctorArgs[parameterName] = parameterValue; + } } - currentDeclared = currentDeclared.BaseType; + + foreach (var namedArg in attr.Properties) + { + namedArgs[namedArg.Name] = namedArg.Argument.Value.ToString(); + } + retr.CustomAuth ??= []; + retr.CustomAuth.Add(new PageRouteAuthCustomAuth(attr.AttributeType.FullName, ctorArgs, namedArgs)); } + } + + return retr; + } + + private static bool IsSubclassOf(TypeDefinition type, string baseTypeName) + { + while (type != null && type.FullName != "System.Object") + { + if (type.FullName == baseTypeName) + { + return true; + } + + type = type.BaseType?.Resolve(); + } - return false; + return false; + } + + private static IEnumerable GetAttributes(IEnumerable customAttributes) + { + return customAttributes ?? []; + } + + static string? getHighestFolderVersion(string inputFolder, string searchPattern = "net*") + { + var versionFolders = Directory.GetDirectories(inputFolder, searchPattern) + .OrderByDescending(v => Version.Parse(Path.GetFileName(v)[3..])) + .ToList(); + + if (versionFolders.Count == 0) + { + throw new DirectoryNotFoundException("No version folders found in debug directory."); } + + var highestVersionFolder = versionFolders.First(); + + return highestVersionFolder; } } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Settings.cs b/GoLive.Generator.RazorPageRoute.Generator/Settings.cs index 20fa7eb..b386c3a 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/Settings.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/Settings.cs @@ -1,19 +1,41 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; -namespace GoLive.Generator.RazorPageRoute.Generator +namespace GoLive.Generator.RazorPageRoute.Generator; + +public class Settings { - public class Settings - { - public string Namespace { get; set; } - public string ClassName { get; set; } + public string Namespace { get; set; } + public string ClassName { get; set; } - public string OutputToFile { get; set; } - public List OutputToFiles { get; set; } = new(); + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List OutputToFiles { get; set; } = new(); - public string DebugOutputFile { get; set; } + public string DebugOutputFile { get; set; } - public bool OutputLastCreatedTime { get; set; } + public bool OutputLastCreatedTime { get; set; } - public bool OutputExtensionMethod { get; set; } - } + public bool OutputExtensionMethod { get; set; } + + public Settings_JSInvokables Invokables { get; set; } = new(); + + public List Auth { get; set; } = []; + public bool OutputIAuthorizeData { get; set; } +} + +public class Settings_JSInvokables +{ + public bool Enabled { get; set; } + + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List OutputToFiles { get; set; } = new(); + public string JSClassName { get; set; } +} + +public class Settings_Auth +{ + public string Attribute { get; set; } + public string PolicyTransformer { get; set; } + public string RolesTransformer { get; set; } + public string AuthenticationSchemeTransformer { get; set; } } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/Slug.cs b/GoLive.Generator.RazorPageRoute.Generator/Slug.cs index 5b18ae1..f75c7b0 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/Slug.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/Slug.cs @@ -2,129 +2,141 @@ using System.Globalization; using System.Text; -namespace GoLive.Generator.RazorPageRoute.Generator +namespace GoLive.Generator.RazorPageRoute.Generator; + +/// +/// Defines a set of utilities for creating slug urls. +/// +public static class Slug { /// - /// Defines a set of utilities for creating slug urls. + /// Creates a slug from the specified text. /// - public static class Slug + /// The text. If null if specified, null will be returned. + /// + /// A slugged text. + /// + public static string Create(string text) { - /// - /// Creates a slug from the specified text. - /// - /// The text. If null if specified, null will be returned. - /// - /// A slugged text. - /// - public static string Create(string text) - { - return Create(text, (SlugOptions)null); - } + return Create(text, (SlugOptions)null); + } - /// - /// Creates a slug from the specified text. - /// - /// The text. If null if specified, null will be returned. - /// The options. May be null. - /// A slugged text. - public static string Create(string text, SlugOptions options) + /// + /// Creates a slug from the specified text. + /// + /// The text. If null if specified, null will be returned. + /// The options. May be null. + /// A slugged text. + public static string Create(string text, SlugOptions options) + { + if (text == null) return null; + + if (options == null) { - if (text == null) return null; + options = new SlugOptions(); + } - if (options == null) - { - options = new SlugOptions(); - } + string normalised; - string normalised; + if (options.EarlyTruncate && options.MaximumLength > 0 && text.Length > options.MaximumLength) + { + normalised = text.Substring(0, options.MaximumLength).Normalize(NormalizationForm.FormD); + } + else + { + normalised = text.Normalize(NormalizationForm.FormD); + } - if (options.EarlyTruncate && options.MaximumLength > 0 && text.Length > options.MaximumLength) - { - normalised = text.Substring(0, options.MaximumLength).Normalize(NormalizationForm.FormD); - } - else - { - normalised = text.Normalize(NormalizationForm.FormD); - } + int max = options.MaximumLength > 0 ? Math.Min(normalised.Length, options.MaximumLength) : normalised.Length; + StringBuilder sb = new StringBuilder(max); - int max = options.MaximumLength > 0 ? Math.Min(normalised.Length, options.MaximumLength) : normalised.Length; - StringBuilder sb = new StringBuilder(max); + for (int i = 0; i < normalised.Length; i++) + { + char c = normalised[i]; + UnicodeCategory uc = char.GetUnicodeCategory(c); - for (int i = 0; i < normalised.Length; i++) + if (options.AllowedUnicodeCategories.Contains(uc) && options.IsAllowed(c)) { - char c = normalised[i]; - UnicodeCategory uc = char.GetUnicodeCategory(c); - - if (options.AllowedUnicodeCategories.Contains(uc) && options.IsAllowed(c)) + switch (uc) { - switch (uc) - { - case UnicodeCategory.UppercaseLetter: - if (options.ToLower) - { - c = options.Culture != null ? char.ToLower(c, options.Culture) : char.ToLowerInvariant(c); - } - - sb.Append(options.Replace(c)); - - break; - - case UnicodeCategory.LowercaseLetter: - if (options.ToUpper) + case UnicodeCategory.UppercaseLetter: + if (options.ToLower) + { + c = options.Culture != null ? char.ToLower(c, options.Culture) : char.ToLowerInvariant(c); + } + + sb.Append(options.Replace(c)); + + break; + + case UnicodeCategory.LowercaseLetter: + if (i == 0) + { + c = options.Culture != null ? char.ToUpper(c, options.Culture) : char.ToUpperInvariant(c); + } + else + { + if (sb[sb.Length - 1] == '_') { c = options.Culture != null ? char.ToUpper(c, options.Culture) : char.ToUpperInvariant(c); } + } + + + if (options.ToUpper) + { + c = options.Culture != null ? char.ToUpper(c, options.Culture) : char.ToUpperInvariant(c); + } - sb.Append(options.Replace(c)); + sb.Append(options.Replace(c)); - break; + break; - default: - sb.Append(options.Replace(c)); + default: + sb.Append(options.Replace(c)); - break; - } - } - else if (uc == UnicodeCategory.NonSpacingMark) - { - // don't add a separator + break; } - else - { - if (options.Separator != null && !EndsWith(sb, options.Separator)) - { - sb.Append(options.Separator); - } - } - - if (options.MaximumLength > 0 && sb.Length >= options.MaximumLength) break; } - - string result = sb.ToString(); - - if (options.MaximumLength > 0 && result.Length > options.MaximumLength) + else if (uc == UnicodeCategory.NonSpacingMark) { - result = result.Substring(0, options.MaximumLength); + // don't add a separator } - - if (!options.CanEndWithSeparator && options.Separator != null && result.EndsWith(options.Separator)) + else { - result = result.Substring(0, result.Length - options.Separator.Length); + if (options.Separator != null && !EndsWith(sb, options.Separator)) + { + sb.Append(options.Separator); + } } - return result.Normalize(NormalizationForm.FormC); + if (options.MaximumLength > 0 && sb.Length >= options.MaximumLength) break; } - private static bool EndsWith(StringBuilder sb, string text) + string result = sb.ToString(); + + if (options.MaximumLength > 0 && result.Length > options.MaximumLength) { - if (sb.Length < text.Length) return false; + result = result.Substring(0, options.MaximumLength); + } - for (int i = 0; i < text.Length; i++) - { - if (sb[sb.Length - 1 - i] != text[text.Length - 1 - i]) return false; - } + if (!options.CanEndWithSeparator && options.Separator != null && result.EndsWith(options.Separator)) + { + result = result.Substring(0, result.Length - options.Separator.Length); + } - return true; + return result.Normalize(NormalizationForm.FormC); + } + + private static bool EndsWith(StringBuilder sb, string text) + { + if (sb.Length < text.Length) return false; + + for (int i = 0; i < text.Length; i++) + { + if (sb[sb.Length - 1 - i] != text[text.Length - 1 - i]) return false; } + + return true; } } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/SlugOptions.cs b/GoLive.Generator.RazorPageRoute.Generator/SlugOptions.cs index 9b99446..ac77858 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/SlugOptions.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/SlugOptions.cs @@ -1,162 +1,161 @@ using System.Collections.Generic; using System.Globalization; -namespace GoLive.Generator.RazorPageRoute.Generator +namespace GoLive.Generator.RazorPageRoute.Generator; + +/// +/// Defines options for the Slug utility class. +/// +public class SlugOptions { /// - /// Defines options for the Slug utility class. + /// Defines the default maximum length. Currently equal to 80. + /// + public const int DefaultMaximumLength = 80; + + /// + /// Defines the default separator. Currently equal to "-". + /// + public const string DefaultSeparator = "_"; + + private bool _toLower; + private bool _toUpper; + + /// + /// Initializes a new instance of the class. /// - public class SlugOptions + public SlugOptions() { - /// - /// Defines the default maximum length. Currently equal to 80. - /// - public const int DefaultMaximumLength = 80; - - /// - /// Defines the default separator. Currently equal to "-". - /// - public const string DefaultSeparator = "_"; - - private bool _toLower; - private bool _toUpper; - - /// - /// Initializes a new instance of the class. - /// - public SlugOptions() - { - MaximumLength = DefaultMaximumLength; - Separator = DefaultSeparator; - AllowedUnicodeCategories = new List(); - AllowedUnicodeCategories.Add(UnicodeCategory.UppercaseLetter); - AllowedUnicodeCategories.Add(UnicodeCategory.LowercaseLetter); - AllowedUnicodeCategories.Add(UnicodeCategory.DecimalDigitNumber); - AllowedRanges = new List>(); - AllowedRanges.Add(new KeyValuePair((short)'a', (short)'z')); - AllowedRanges.Add(new KeyValuePair((short)'A', (short)'Z')); - AllowedRanges.Add(new KeyValuePair((short)'0', (short)'9')); - } + MaximumLength = DefaultMaximumLength; + Separator = DefaultSeparator; + AllowedUnicodeCategories = new List(); + AllowedUnicodeCategories.Add(UnicodeCategory.UppercaseLetter); + AllowedUnicodeCategories.Add(UnicodeCategory.LowercaseLetter); + AllowedUnicodeCategories.Add(UnicodeCategory.DecimalDigitNumber); + AllowedRanges = new List>(); + AllowedRanges.Add(new KeyValuePair((short)'a', (short)'z')); + AllowedRanges.Add(new KeyValuePair((short)'A', (short)'Z')); + AllowedRanges.Add(new KeyValuePair((short)'0', (short)'9')); + } - /// - /// Gets the allowed unicode categories list. - /// - /// - /// The allowed unicode categories list. - /// - public virtual IList AllowedUnicodeCategories { get; private set; } - - /// - /// Gets the allowed ranges list. - /// - /// - /// The allowed ranges list. - /// - public virtual IList> AllowedRanges { get; private set; } - - /// - /// Gets or sets the maximum length. - /// - /// - /// The maximum length. - /// - public virtual int MaximumLength { get; set; } - - /// - /// Gets or sets the separator. - /// - /// - /// The separator. - /// - public virtual string Separator { get; set; } - - /// - /// Gets or sets the culture for case conversion. - /// - /// - /// The culture. - /// - public virtual CultureInfo Culture { get; set; } - - /// - /// Gets or sets a value indicating whether the string can end with a separator string. - /// - /// - /// true if the string can end with a separator string; otherwise, false. - /// - public virtual bool CanEndWithSeparator { get; set; } - - /// - /// Gets or sets a value indicating whether the string is truncated before normalization. - /// - /// - /// true if the string is truncated before normalization; otherwise, false. - /// - public virtual bool EarlyTruncate { get; set; } - - /// - /// Gets or sets a value indicating whether to lowercase the resulting string. - /// - /// - /// true if the resulting string must be lowercased; otherwise, false. - /// - public virtual bool ToLower - { - get { return _toLower; } - set - { - _toLower = value; + /// + /// Gets the allowed unicode categories list. + /// + /// + /// The allowed unicode categories list. + /// + public virtual IList AllowedUnicodeCategories { get; private set; } - if (_toLower) - { - _toUpper = false; - } - } - } + /// + /// Gets the allowed ranges list. + /// + /// + /// The allowed ranges list. + /// + public virtual IList> AllowedRanges { get; private set; } + + /// + /// Gets or sets the maximum length. + /// + /// + /// The maximum length. + /// + public virtual int MaximumLength { get; set; } + + /// + /// Gets or sets the separator. + /// + /// + /// The separator. + /// + public virtual string Separator { get; set; } + + /// + /// Gets or sets the culture for case conversion. + /// + /// + /// The culture. + /// + public virtual CultureInfo Culture { get; set; } - /// - /// Gets or sets a value indicating whether to uppercase the resulting string. - /// - /// - /// true if the resulting string must be uppercased; otherwise, false. - /// - public virtual bool ToUpper + /// + /// Gets or sets a value indicating whether the string can end with a separator string. + /// + /// + /// true if the string can end with a separator string; otherwise, false. + /// + public virtual bool CanEndWithSeparator { get; set; } + + /// + /// Gets or sets a value indicating whether the string is truncated before normalization. + /// + /// + /// true if the string is truncated before normalization; otherwise, false. + /// + public virtual bool EarlyTruncate { get; set; } + + /// + /// Gets or sets a value indicating whether to lowercase the resulting string. + /// + /// + /// true if the resulting string must be lowercased; otherwise, false. + /// + public virtual bool ToLower + { + get { return _toLower; } + set { - get { return _toUpper; } - set - { - _toUpper = value; + _toLower = value; - if (_toUpper) - { - _toLower = false; - } + if (_toLower) + { + _toUpper = false; } } + } - /// - /// Determines whether the specified character is allowed. - /// - /// The character. - /// true if the character is allowed; false otherwise. - public virtual bool IsAllowed(char character) + /// + /// Gets or sets a value indicating whether to uppercase the resulting string. + /// + /// + /// true if the resulting string must be uppercased; otherwise, false. + /// + public virtual bool ToUpper + { + get { return _toUpper; } + set { - foreach (var p in AllowedRanges) + _toUpper = value; + + if (_toUpper) { - if (character >= p.Key && character <= p.Value) return true; + _toLower = false; } - - return false; } + } - /// - /// Replaces the specified character by a given string. - /// - /// The character to replace. - /// a string. - public virtual string Replace(char character) + /// + /// Determines whether the specified character is allowed. + /// + /// The character. + /// true if the character is allowed; false otherwise. + public virtual bool IsAllowed(char character) + { + foreach (var p in AllowedRanges) { - return character.ToString(); + if (character >= p.Key && character <= p.Value) return true; } + + return false; + } + + /// + /// Replaces the specified character by a given string. + /// + /// The character to replace. + /// a string. + public virtual string Replace(char character) + { + return character.ToString(); } } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/SourceStringBuilder.cs b/GoLive.Generator.RazorPageRoute.Generator/SourceStringBuilder.cs index 964846a..be7889e 100644 --- a/GoLive.Generator.RazorPageRoute.Generator/SourceStringBuilder.cs +++ b/GoLive.Generator.RazorPageRoute.Generator/SourceStringBuilder.cs @@ -3,77 +3,76 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -namespace GoLive.Generator.RazorPageRoute.Generator +namespace GoLive.Generator.RazorPageRoute.Generator; + +public class SourceStringBuilder { - public class SourceStringBuilder - { - private readonly string SingleIndent = new string(' ', 4); + private readonly string SingleIndent = new string(' ', 4); - public int IndentLevel = 0; - private readonly StringBuilder _stringBuilder; + public int IndentLevel = 0; + private readonly StringBuilder _stringBuilder; - public SourceStringBuilder() - { - _stringBuilder = new StringBuilder(); - } + public SourceStringBuilder() + { + _stringBuilder = new StringBuilder(); + } - public void IncreaseIndent() - { - IndentLevel++; - } + public void IncreaseIndent() + { + IndentLevel++; + } - public void DecreaseIndent() - { - IndentLevel--; - } + public void DecreaseIndent() + { + IndentLevel--; + } - public void AppendOpenCurlyBracketLine() - { - AppendLine("{"); - IncreaseIndent(); - } + public void AppendOpenCurlyBracketLine() + { + AppendLine("{"); + IncreaseIndent(); + } - public void AppendCloseCurlyBracketLine() - { - DecreaseIndent(); - AppendLine("}"); - } + public void AppendCloseCurlyBracketLine() + { + DecreaseIndent(); + AppendLine("}"); + } - public void Append(string text, bool indent = true) + public void Append(string text, bool indent = true) + { + if (indent) { - if (indent) - { - AppendIndent(); - } - - _stringBuilder.Append(text); + AppendIndent(); } - public void AppendIndent() - { - for (int i = 0; i < IndentLevel; i++) - { - _stringBuilder.Append(SingleIndent); - } - } + _stringBuilder.Append(text); + } - public void AppendLine() + public void AppendIndent() + { + for (int i = 0; i < IndentLevel; i++) { - _stringBuilder.Append(Environment.NewLine); + _stringBuilder.Append(SingleIndent); } + } - public void AppendLine(string text) - { - Append(text); - AppendLine(); - } + public void AppendLine() + { + _stringBuilder.Append(Environment.NewLine); + } - public override string ToString() - { - var text = _stringBuilder.ToString(); - return string.IsNullOrWhiteSpace(text) - ? string.Empty - : CSharpSyntaxTree.ParseText(text).GetRoot().NormalizeWhitespace().SyntaxTree.GetText().ToString(); - } + public void AppendLine(string text) + { + Append(text); + AppendLine(); + } + + public override string ToString() + { + var text = _stringBuilder.ToString(); + return string.IsNullOrWhiteSpace(text) + ? string.Empty + : CSharpSyntaxTree.ParseText(text).GetRoot().NormalizeWhitespace().SyntaxTree.GetText().ToString(); } } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/StringOrArrayJsonConverter.cs b/GoLive.Generator.RazorPageRoute.Generator/StringOrArrayJsonConverter.cs new file mode 100644 index 0000000..7117961 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Generator/StringOrArrayJsonConverter.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GoLive.Generator.RazorPageRoute.Generator; + +public class StringOrArrayJsonConverter : JsonConverter> +{ + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + // Single string case + return new List { reader.GetString()! }; + } + else if (reader.TokenType == JsonTokenType.StartArray) + { + // Array of strings case + var strings = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType == JsonTokenType.String) + { + strings.Add(reader.GetString()!); + } + else + { + throw new JsonException("Expected a string in the array."); + } + } + return strings; + } + throw new JsonException("Expected a string or an array of strings."); + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + // Always serialize as an array of strings + writer.WriteStartArray(); + foreach (var str in value) + { + writer.WriteStringValue(str); + } + writer.WriteEndArray(); + } +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/StringSegmentAccumulator.cs b/GoLive.Generator.RazorPageRoute.Generator/StringSegmentAccumulator.cs deleted file mode 100644 index 6eea8fe..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/StringSegmentAccumulator.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - internal struct StringSegmentAccumulator - { - private int count; - private ReadOnlyMemory _single; - private List>? _multiple; - - public ReadOnlyMemory this[int index] - { - get - { - if (index >= count) - { - throw new IndexOutOfRangeException(); - } - - return count == 1 ? _single : _multiple![index]; - } - } - - public int Count => count; - - public void SetSingle(ReadOnlyMemory value) - { - _single = value; - - if (count != 1) - { - if (count > 1) - { - _multiple = null; - } - - count = 1; - } - } - - public void Add(ReadOnlyMemory value) - { - switch (count++) - { - case 0: - _single = value; - break; - case 1: - _multiple = new(); - _multiple.Add(_single); - _multiple.Add(value); - _single = default; - break; - default: - _multiple!.Add(value); - break; - } - } - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/TemplateParser.cs b/GoLive.Generator.RazorPageRoute.Generator/TemplateParser.cs deleted file mode 100644 index fcfefee..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/TemplateParser.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - internal class TemplateParser - { - public static readonly char[] InvalidParameterNameCharacters = - new char[] { '{', '}', '=', '.' }; - - internal static RouteTemplate ParseTemplate(string template) - { - var originalTemplate = template; - template = template.Trim('/'); - if (template == string.Empty) - { - // Special case "/"; - return new RouteTemplate("/", Array.Empty()); - } - - var segments = template.Split('/'); - var templateSegments = new TemplateSegment[segments.Length]; - for (int i = 0; i < segments.Length; i++) - { - var segment = segments[i]; - if (string.IsNullOrEmpty(segment)) - { - throw new InvalidOperationException( - $"Invalid template '{template}'. Empty segments are not allowed."); - } - - if (segment[0] != '{') - { - if (segment[segment.Length - 1] == '}') - { - throw new InvalidOperationException( - $"Invalid template '{template}'. Missing '{{' in parameter segment '{segment}'."); - } - if (segment.Substring(segment.Length - 1) == "?") - { - throw new InvalidOperationException( - $"Invalid template '{template}'. '?' is not allowed in literal segment '{segment}'."); - } - templateSegments[i] = new TemplateSegment(originalTemplate, segment, isParameter: false); - } - else - { - if (segment[segment.Length - 1] != '}') - { - throw new InvalidOperationException( - $"Invalid template '{template}'. Missing '}}' in parameter segment '{segment}'."); - } - - if (segment.Length < 3) - { - throw new InvalidOperationException( - $"Invalid template '{template}'. Empty parameter name in segment '{segment}' is not allowed."); - } - - var invalidCharacter = segment.IndexOfAny(InvalidParameterNameCharacters, 1, segment.Length - 2); - if (invalidCharacter != -1) - { - throw new InvalidOperationException( - $"Invalid template '{template}'. The character '{segment[invalidCharacter]}' in parameter segment '{segment}' is not allowed."); - } - - templateSegments[i] = new TemplateSegment(originalTemplate, segment.Substring(1, segment.Length - 2), isParameter: true); - } - } - - for (int i = 0; i < templateSegments.Length; i++) - { - var currentSegment = templateSegments[i]; - - if (currentSegment.IsCatchAll && i != templateSegments.Length - 1) - { - throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter can only appear as the last segment of the route template."); - } - - if (!currentSegment.IsParameter) - { - continue; - } - - for (int j = i + 1; j < templateSegments.Length; j++) - { - var nextSegment = templateSegments[j]; - - if (currentSegment.IsOptional && !nextSegment.IsOptional && !nextSegment.IsCatchAll) - { - throw new InvalidOperationException($"Invalid template '{template}'. Non-optional parameters or literal routes cannot appear after optional parameters."); - } - - if (string.Equals(currentSegment.Value, nextSegment.Value, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException( - $"Invalid template '{template}'. The parameter '{currentSegment}' appears multiple times."); - } - } - } - - return new RouteTemplate(template, templateSegments); - } - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/TemplateSegment.cs b/GoLive.Generator.RazorPageRoute.Generator/TemplateSegment.cs deleted file mode 100644 index 04f49df..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/TemplateSegment.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - internal class TemplateSegment - { - public TemplateSegment(string template, string segment, bool isParameter) - { - IsParameter = isParameter; - - IsCatchAll = isParameter && segment.StartsWith('*'.ToString()); - - if (IsCatchAll) - { - // Only one '*' currently allowed - Value = segment.Substring(1); - - var invalidCharacterIndex = Value.IndexOf('*'); - if (invalidCharacterIndex != -1) - { - throw new InvalidOperationException($"Invalid template '{template}'. A catch-all parameter may only have one '*' at the beginning of the segment."); - } - } - else - { - Value = segment; - } - - // Process segments that parameters that do not contain a token separating a type constraint. - if (IsParameter) - { - if (Value.IndexOf(':') < 0) - { - - // Set the IsOptional flag to true for segments that contain - // a parameter with no type constraints but optionality set - // via the '?' token. - var questionMarkIndex = Value.IndexOf('?'); - if (questionMarkIndex == Value.Length - 1) - { - IsOptional = true; - Value = Value.Substring(0, Value.Length - 1); - } - // If the `?` optional marker shows up in the segment but not at the very end, - // then throw an error. - else if (questionMarkIndex >= 0) - { - throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}'. '?' character can only appear at the end of parameter name."); - } - - Constraints = Array.Empty(); - } - else - { - var tokens = Value.Split(':'); - if (tokens[0].Length == 0) - { - throw new ArgumentException($"Malformed parameter '{segment}' in route '{template}' has no name before the constraints list."); - } - - Value = tokens[0]; - IsOptional = tokens[tokens.Length - 1].EndsWith('?'.ToString()); - if (IsOptional) - { - tokens[tokens.Length - 1] = tokens[tokens.Length - 1].Substring(0, tokens[tokens.Length - 1].Length - 1); - } - - Constraints = new UrlValueConstraint[tokens.Length - 1]; - for (var i = 1; i < tokens.Length; i++) - { - Constraints[i - 1] = RouteConstraint.Parse(template, segment, tokens[i]); - } - } - } - else - { - Constraints = Array.Empty(); - } - - if (IsParameter) - { - if (IsOptional && IsCatchAll) - { - throw new InvalidOperationException($"Invalid segment '{segment}' in route '{template}'. A catch-all parameter cannot be marked optional."); - } - - // Moving the check for this here instead of TemplateParser so we can allow catch-all. - // We checked for '*' up above specifically for catch-all segments, this one checks for all others - if (Value.IndexOf('*') != -1) - { - throw new InvalidOperationException($"Invalid template '{template}'. The character '*' in parameter segment '{{{segment}}}' is not allowed."); - } - } - } - - // The value of the segment. The exact text to match when is a literal. - // The parameter name when its a segment - public string Value { get; } - - public bool IsParameter { get; } - - public bool IsOptional { get; } - - public bool IsCatchAll { get; } - - public UrlValueConstraint[] Constraints { get; } - - public bool Match(string pathSegment, out object? matchedParameterValue) - { - if (IsParameter) - { - matchedParameterValue = pathSegment; - - foreach (var constraint in Constraints) - { - if (!constraint.TryParse(pathSegment, out matchedParameterValue)) - { - return false; - } - } - - return true; - } - else - { - matchedParameterValue = null; - return string.Equals(Value, pathSegment, StringComparison.OrdinalIgnoreCase); - } - } - - public override string ToString() => this switch - { - { IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: 0 } } => $"{{{Value}}}", - { IsParameter: true, IsOptional: false, IsCatchAll: false, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':'.ToString(), (object[])Constraints)}}}", - { IsParameter: true, IsOptional: true, Constraints: { Length: 0 } } => $"{{{Value}?}}", - { IsParameter: true, IsOptional: true, Constraints: { Length: > 0 } } => $"{{{Value}:{string.Join(':'.ToString(), (object[])Constraints)}?}}", - { IsParameter: true, IsCatchAll: true, Constraints: { Length: 0 } } => $"{{*{Value}}}", - { IsParameter: true, IsCatchAll: true, Constraints: { Length: > 0 } } => $"{{*{Value}:{string.Join(':'.ToString(), (object[])Constraints)}?}}", - { IsParameter: false } => Value, - _ => throw new InvalidOperationException("Invalid template segment.") - }; - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Generator/UrlValueConstraint.cs b/GoLive.Generator.RazorPageRoute.Generator/UrlValueConstraint.cs deleted file mode 100644 index 3fcdfca..0000000 --- a/GoLive.Generator.RazorPageRoute.Generator/UrlValueConstraint.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; - -namespace GoLive.Generator.RazorPageRoute.Generator -{ - /// - /// Shared logic for parsing tokens from route values and querystring values. - /// - internal abstract class UrlValueConstraint - { - public delegate bool TryParseDelegate(string str, out T result); - - - - private static readonly ConcurrentDictionary _cachedInstances = new(); - - public static bool TryGetByTargetType(Type targetType, out UrlValueConstraint result) - { - if (!_cachedInstances.TryGetValue(targetType, out result)) - { - result = Create(targetType); - if (result is null) - { - return false; - } - - _cachedInstances.TryAdd(targetType, result); - } - - return true; - } - - private static bool TryParse(string str, out string result) - { - result = str.ToString(); - return true; - } - - private static bool TryParse(string str, out DateTime result) - => DateTime.TryParse(str.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out result); - - private static bool TryParse(string str, out decimal result) - => decimal.TryParse(str.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out result); - - private static bool TryParse(string str, out double result) - => double.TryParse(str.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out result); - - private static bool TryParse(string str, out float result) - => float.TryParse(str.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out result); - - private static bool TryParse(string str, out int result) - => int.TryParse(str.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result); - - private static bool TryParse(string str, out long result) - => long.TryParse(str.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result); - - private static UrlValueConstraint? Create(Type targetType) => targetType switch - { - var x when x == typeof(string) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(bool) => new TypedUrlValueConstraint(bool.TryParse), - var x when x == typeof(bool?) => new NullableTypedUrlValueConstraint(bool.TryParse), - var x when x == typeof(DateTime) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(DateTime?) => new NullableTypedUrlValueConstraint(TryParse), - var x when x == typeof(decimal) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(decimal?) => new NullableTypedUrlValueConstraint(TryParse), - var x when x == typeof(double) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(double?) => new NullableTypedUrlValueConstraint(TryParse), - var x when x == typeof(float) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(float?) => new NullableTypedUrlValueConstraint(TryParse), - var x when x == typeof(Guid) => new TypedUrlValueConstraint(Guid.TryParse), - var x when x == typeof(Guid?) => new NullableTypedUrlValueConstraint(Guid.TryParse), - var x when x == typeof(int) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(int?) => new NullableTypedUrlValueConstraint(TryParse), - var x when x == typeof(long) => new TypedUrlValueConstraint(TryParse), - var x when x == typeof(long?) => new NullableTypedUrlValueConstraint(TryParse), - var x => null - }; - - public abstract Type GetConstraintType(); - - public abstract bool TryParse(string value, out object result); - - public abstract object? Parse(string value, string destinationNameForMessage); - - public abstract Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage); - - private class TypedUrlValueConstraint : UrlValueConstraint - { - private readonly TryParseDelegate _parser; - - public TypedUrlValueConstraint(TryParseDelegate parser) - { - _parser = parser; - } - - public override Type GetConstraintType() - { - return typeof(T); - } - - public override bool TryParse(string value, out object result) - { - if (_parser(value, out var typedResult)) - { - result = typedResult!; - return true; - } - else - { - result = null; - return false; - } - } - - public override object? Parse(string value, string destinationNameForMessage) - { - if (!_parser(value, out var parsedValue)) - { - throw new InvalidOperationException($"Cannot parse the value '{value.ToString()}' as type '{typeof(T)}' for '{destinationNameForMessage}'."); - } - - return parsedValue; - } - - public override Array ParseMultiple(StringSegmentAccumulator values, string destinationNameForMessage) - { - var count = values.Count; - if (count == 0) - { - return Array.Empty(); - } - - var result = new T?[count]; - - for (var i = 0; i < count; i++) - { - if (!_parser(values[i].ToString(), out result[i])) - { - throw new InvalidOperationException($"Cannot parse the value '{values[i]}' as type '{typeof(T)}' for '{destinationNameForMessage}'."); - } - } - - return result; - } - } - - private sealed class NullableTypedUrlValueConstraint : TypedUrlValueConstraint where T : struct - { - public NullableTypedUrlValueConstraint(TryParseDelegate parser) - : base(SupportNullable(parser)) - { - } - - private static TryParseDelegate SupportNullable(TryParseDelegate parser) - { - return TryParseNullable; - - bool TryParseNullable(string value, out T? result) - { - if (string.IsNullOrWhiteSpace(value)) - { - result = default; - return true; - } - else if (parser(value, out var parsedValue)) - { - result = parsedValue; - return true; - } - else - { - result = default; - return false; - } - } - } - } - } -} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/App.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/App.razor similarity index 100% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/App.razor rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/App.razor diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly2/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly2.csproj b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly.csproj similarity index 59% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly2/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly2.csproj rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly.csproj index 222d8cb..c742073 100644 --- a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly2/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly2.csproj +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly.csproj @@ -4,18 +4,29 @@ net6.0 enable enable - 10.0 - false + latest + false true $(BaseIntermediateOutputPath)Generated - + + + + + + + + - + + + true + + diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/PageRoute.cs b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/PageRoute.cs similarity index 50% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/PageRoute.cs rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/PageRoute.cs index a702b6c..e6485dd 100644 --- a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/PageRoute.cs +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/PageRoute.cs @@ -1,16 +1,18 @@ -// This file was generated on Sat, 11 Nov 2023 12:21:22 GMT +// This file was generated on Sun, 08 Dec 2024 20:43:14 GMT using System; using System.Net.Http; using System.Threading.Tasks; using System.Net.Http.Json; using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components; -namespace GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly +namespace GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly { public static partial class PageRoutes { - public static string counter(string? QSInput = default) + public static string Counter(System.String QSInput = default) { string url = "/counter"; Dictionary queryString = new(); @@ -23,7 +25,7 @@ public static string counter(string? QSInput = default) return url; } - public static void counter(this NavigationManager manager, string? QSInput = default, bool forceLoad = false, bool replace = false) + public static void Counter(this NavigationManager manager, System.String QSInput = default, bool forceLoad = false, bool replace = false) { string url = "/counter"; Dictionary queryString = new(); @@ -36,7 +38,7 @@ public static void counter(this NavigationManager manager, string? QSInput = def manager.NavigateTo(url, forceLoad, replace); } - public static string counter_view(string id, string? QSInput = default) + public static string Counter_View(string id, System.String QSInput = default) { string url = $"/counter/view/{id.ToString()}"; Dictionary queryString = new(); @@ -49,7 +51,7 @@ public static string counter_view(string id, string? QSInput = default) return url; } - public static void counter_view(this NavigationManager manager, string id, string? QSInput = default, bool forceLoad = false, bool replace = false) + public static void Counter_View(this NavigationManager manager, string id, System.String QSInput = default, bool forceLoad = false, bool replace = false) { string url = $"/counter/view/{id.ToString()}"; Dictionary queryString = new(); @@ -62,7 +64,7 @@ public static void counter_view(this NavigationManager manager, string id, strin manager.NavigateTo(url, forceLoad, replace); } - public static string counter_viewbyid(System.Int32 id, string? QSInput = default) + public static string Counter_Viewbyid(System.Int32 id, System.String QSInput = default) { string url = $"/counter/viewbyid/{id.ToString()}"; Dictionary queryString = new(); @@ -75,7 +77,7 @@ public static string counter_viewbyid(System.Int32 id, string? QSInput = default return url; } - public static void counter_viewbyid(this NavigationManager manager, System.Int32 id, string? QSInput = default, bool forceLoad = false, bool replace = false) + public static void Counter_Viewbyid(this NavigationManager manager, System.Int32 id, System.String QSInput = default, bool forceLoad = false, bool replace = false) { string url = $"/counter/viewbyid/{id.ToString()}"; Dictionary queryString = new(); @@ -88,13 +90,61 @@ public static void counter_viewbyid(this NavigationManager manager, System.Int32 manager.NavigateTo(url, forceLoad, replace); } - public static string fetchdata() + public static string CustomAuthPage() + { + string url = "/CustomAuthPage"; + return url; + } + + public class CustomAuthPage_AuthData : IAuthorizeData + { + public string Policy { get; set; } = String.Empty; + public string Roles { get; set; } = "Admin, superuser"; + public string AuthenticationSchemes { get; set; } = String.Empty; + } + + /// + /// Page Requires Authentication, Custom Authentication Provider (CustomAuth) + /// Custom Authentication ProviderName: GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly.CustomAuth + /// Custom Auth Constructor Params: [roles, Admin] + /// Custom Auth Named Params: + /// + public static void CustomAuthPage(this NavigationManager manager, bool forceLoad = false, bool replace = false) + { + string url = "/CustomAuthPage"; + manager.NavigateTo(url, forceLoad, replace); + } + + public static string AuthPage() + { + string url = "/AuthPage"; + return url; + } + + public class AuthPage_AuthData : IAuthorizeData + { + public string Policy { get; set; } = String.Empty; + public string Roles { get; set; } = "Admin"; + public string AuthenticationSchemes { get; set; } = String.Empty; + } + + /// + /// Page Requires Authentication + /// Roles: Admin + /// + public static void AuthPage(this NavigationManager manager, bool forceLoad = false, bool replace = false) + { + string url = "/AuthPage"; + manager.NavigateTo(url, forceLoad, replace); + } + + public static string Fetchdata() { string url = "/fetchdata"; return url; } - public static void fetchdata(this NavigationManager manager, bool forceLoad = false, bool replace = false) + public static void Fetchdata(this NavigationManager manager, bool forceLoad = false, bool replace = false) { string url = "/fetchdata"; manager.NavigateTo(url, forceLoad, replace); @@ -111,5 +161,17 @@ public static void Home(this NavigationManager manager, bool forceLoad = false, string url = "/"; manager.NavigateTo(url, forceLoad, replace); } + + public static string NotFound(System.String path) + { + string url = $"/{path.ToString()}"; + return url; + } + + public static void NotFound(this NavigationManager manager, System.String path, bool forceLoad = false, bool replace = false) + { + string url = $"/{path.ToString()}"; + manager.NavigateTo(url, forceLoad, replace); + } } } \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/Counter.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/Counter.razor similarity index 75% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/Counter.razor rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/Counter.razor index 43c048f..42c0569 100644 --- a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/Counter.razor +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/Counter.razor @@ -20,4 +20,10 @@ { currentCount++; } + + [JSInvokable("GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly.Pages.Counter.Test")] + public void Test() + { + + } } diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/CustomAuthenticated.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/CustomAuthenticated.razor new file mode 100644 index 0000000..bc4665e --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/CustomAuthenticated.razor @@ -0,0 +1,7 @@ +@page "/CustomAuthPage" +@attribute [CustomAuth(new[] { "Admin" })] +@inject NavigationManager navi + +Index + +

Hello, world!

diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/DefaultAuthenticated.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/DefaultAuthenticated.razor new file mode 100644 index 0000000..25eea8a --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/DefaultAuthenticated.razor @@ -0,0 +1,8 @@ +@page "/AuthPage" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Roles = "Admin")] +@inject NavigationManager navi + +Index + +

Hello, world!

diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/DefaultAuthenticatedWithVariablename.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/DefaultAuthenticatedWithVariablename.razor new file mode 100644 index 0000000..a39c7f4 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/DefaultAuthenticatedWithVariablename.razor @@ -0,0 +1,8 @@ +@page "/AuthPage" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Roles = Const.Admin)] +@inject NavigationManager navi + +Index + +

Hello, world!

diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/FetchData.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/FetchData.razor similarity index 100% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/FetchData.razor rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/FetchData.razor diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/Index.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/Index.razor similarity index 100% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Pages/Index.razor rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/Index.razor diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/NotFound.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/NotFound.razor new file mode 100644 index 0000000..6fd2447 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Pages/NotFound.razor @@ -0,0 +1,13 @@ +@page "/{*path:nonfile}" +@inject NavigationManager navi + +Index + +

Hello, world!

+ +Welcome to your new app. + + + + +@*@navi.counter_viewbyid(32)*@ \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Program.cs b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Program.cs similarity index 86% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Program.cs rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Program.cs index f69ecf3..791e2fb 100644 --- a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Program.cs +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Program.cs @@ -1,4 +1,4 @@ -using GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly; +using GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Properties/launchSettings.json b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Properties/launchSettings.json similarity index 92% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Properties/launchSettings.json rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Properties/launchSettings.json index 385dd05..57376ff 100644 --- a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Properties/launchSettings.json +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly": { + "GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/RazorPageRoutes.json b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/RazorPageRoutes.json new file mode 100644 index 0000000..6675c69 --- /dev/null +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/RazorPageRoutes.json @@ -0,0 +1,19 @@ +{ + "Namespace": "GoLive.Generator.RazorPageRoute.Tests.BlazorWebassembly", + "ClassName": "PageRoutes", + "OutputToFile": "PageRoute.cs", + "OutputLastCreatedTime": true, + "OutputExtensionMethod": true, + "OutputIAuthorizeData": true, + "Invokables": { + "Enabled": true, + "JSClassName": "INVOKABLES", + "OutputToFile": "wwwroot\\jsinvokable.js" + }, + "Auth": [ + { + "Attribute": "GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly.CustomAuth", + "RolesTransformer": " var roles = ConstructorParameters[\"roles\"].Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);\r\n\r\n \/\/ Create a List to store the roles\r\n var rolesList = new List(roles);\r\n\r\n \/\/ Add the \"superuser\" role to the list\r\n rolesList.Add(\"superuser\");\r\n\r\n \/\/ Return the processed list of roles\r\n return rolesList;" + } + ] +} \ No newline at end of file diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/MainLayout.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/MainLayout.razor similarity index 100% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/MainLayout.razor rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/MainLayout.razor diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/MainLayout.razor.css b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/MainLayout.razor.css similarity index 100% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/MainLayout.razor.css rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/MainLayout.razor.css diff --git a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/NavMenu.razor b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/NavMenu.razor similarity index 97% rename from GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/NavMenu.razor rename to GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/NavMenu.razor index 46ad4ce..6022a51 100644 --- a/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAsssembly/Shared/NavMenu.razor +++ b/GoLive.Generator.RazorPageRoute.Tests.BlazorWebAssembly/Shared/NavMenu.razor @@ -1,6 +1,6 @@