Private/Classes/20-ControllerEntry.ps1
<# Object that manages a user controller class. #> class ControllerEntry { # The user class this controller is associated with hidden [type]$ControllerClass # The routes declared by the user class hidden [RouteEntry[]]$Routes # Route prefix for this controller [string]$Prefix # Full name of the controller class [string]$ControllerClassName <# Constructor - takes type of a user controller #> ControllerEntry([type]$controllerClass) { # Read controller attribute from user class $controllerAttr = $controllerClass.GetCustomAttributes('Controller') | Where-Object { $_ -is [Controller] } if (@($controllerAttr).Length -ne 1) { throw "$($controllerClass.Name): Invalid number of [Controller] attributes ($(@($controllerAttr).Length))" } $this.ControllerClass = $controllerClass $this.ControllerClassName = $controllerClass.FullName # Determine route prefix $this.Prefix = $( if ([string]::IsNullOrEmpty($controllerAttr.Prefix)) { # If the controller class was declared with [Controller()], i.e. no defined prefix... $controllerLength = 'Controller'.Length $className = $controllerClass.Name if ($className.Length -gt $controllerLength -and $className.EndsWith('Controller', 'OrdinalIgnoreCase')) { # If name of controller class ends with 'Controller' then the prefix is the class name in lowercase with 'Controller' removed. # e.g. MyController -> /my "/$($className.Substring(0, $className.Length - $controllerLength).ToLowerInvariant())" } else { "/$($className.ToLowerInvariant())" } } else { $controllerAttr.Prefix } ) # Build the route list from the controller class $routeList = [System.Collections.Generic.List[RouteEntry]]::new() $controllerClass.GetMethods() | ForEach-Object { $method = $_ $attr = $method.GetCustomAttributes($true) | Where-Object { $_ -is [RestAttribute] } # If method has any rest attribute on it, then process if ($attr) { # String representation of controller method for messages $methodName = Format-MethodSignature -Method $method # Get verb and route $verbs = $attr | Where-Object { $_ -is [HttpRequestMethod] } $routes = $attr | Where-Object { $_ -is [Route] } if (@($verbs).Count -gt 1) { # Seems that PowerShell ignores the AttrinuteUsage attribute throw "$methodName - Multiple HTTP Verbs not allowed." } elseif (@($verbs).Count -eq 0) { # Default GET $verbs = New-Object HttpGet } if (($routes | Measure-Object).Count -gt 1) { throw "$methodName - Multiple routes not allowed." } elseif (($routes | Measure-Object).Count -eq 0) { # It has a verb or we wouldn't get here throw "$methodName - Missing route attribute" } $routeList.Add([RouteEntry]::new($this.Prefix, $method, $verbs, $routes)) } } # Test for duplicate routes $routeList | Group-Object -Property { $_.GetHashCode() } | Foreach-Object { # Finally for duplicate route/verb in the classes we found if ($_.Count -gt 1) { $methods = ($_.Group | ForEach-Object { $_.ToString() }) -join ', ' throw "($methods) define the same route: $($_.Group[0].RequestMethod) $($_.Group[0].Route)" } } $this.Routes = $routeList.ToArray() } <# Given a request path that matches this controller class, select the best route on this controller that matches the path Return zero or one route. If one route, it will be the one with the highest score See RouteEntry::MatchScore() #> [RouteEntry]GetRoute([string]$requestMethod, [string]$path) { $candidates = $this.Routes | Where-Object { if ($requestMethod -ieq 'HEAD') { # Redirect HEAD requests to GET routes $_.RequestMethod -ieq 'GET' } else { $_.RequestMethod -ieq $requestMethod } } | ForEach-Object { New-Object PSObject -Property @{ Score = $_.MatchScore($path) Route = $_ } } return $candidates | Where-Object {$_.Score -gt 0 } | Sort-Object -Property Score -Descending | Select-Object -First 1 | Select-Object -ExpandProperty Route } [string[]]GetRouteOptions([string]$Path) { $candidates = $this.Routes | ForEach-Object { New-Object PSObject -Property @{ Score = $_.MatchScore($path) Route = $_ } } | Group-Object -Property Score | Sort-Object -Property @{ Expression = { [int]($_.Name) } ; Descending = $true } | Select-Object -First 1 if (($candidates | Measure-Object).Count -eq 0) { # No matching routes return $null } $retval = @($candidates.Group.Route.RequestMethod) + 'OPTIONS' if ($retval -icontains 'GET') { $retval += 'HEAD' } return $retval | Sort-Object -Unique } [string]ToString() { return "$($this.ControllerClass.Name) [$($this.Prefix)]" } <# We use the hash code to detect controllers defining the same prefix in New-ControllerTable #> [int]GetHashCode() { return $this.Prefix.ToLowerInvariant().GetHashCode() } [bool]HasRoutes() { return $null -ne $this.Routes -and $this.Routes.Length -gt 0 } } |